Existe la opinión de que C # no tiene cabida en las tareas computacionales, y esta opinión es bastante razonable: el compilador JIT se ve obligado a compilar y optimizar el código sobre la marcha durante la ejecución del programa con retrasos mínimos, simplemente no tiene oportunidad de gastar más recursos computacionales para generar código más eficiente , a diferencia del compilador de C ++, que puede tardar minutos e incluso horas en este tema.
Sin embargo, en los últimos años, la eficiencia del compilador JIT ha aumentado notablemente y se han incorporado varios chips útiles al propio marco, por ejemplo, los intrínsecos .
Y luego me pregunté: ¿es posible en 2020, usando .NET 5.0, escribir código que no sea muy inferior en rendimiento a C ++? Resultó que puedes.
Motivación
Me dedico al desarrollo de algoritmos de procesamiento de imágenes y a un nivel bastante bajo. Es decir, no se trata de hacer malabarismos con ladrillos en Python, sino del desarrollo de algo nuevo y, preferiblemente, productivo. El código Python lleva un tiempo inaceptablemente largo, mientras que el uso de C ++ conduce a una disminución en la velocidad de desarrollo. El equilibrio óptimo entre productividad y rendimiento para tales tareas se logra utilizando C # y Java. En confirmación de mis palabras, el proyecto Fiji .
Anteriormente, utilicé C # para la creación de prototipos y reescribí algoritmos listos para usar que son críticos para el rendimiento en C ++, los introduje en la biblioteca y extraí la biblioteca de C #. Pero en este caso, la portabilidad se resintió y no fue muy conveniente depurar el código.
Pero eso fue hace mucho tiempo, desde entonces .NET ha dado un paso adelante, y me preguntaba si podría abandonar la biblioteca nativa de C ++ y cambiar por completo a C #.
Guión
Compararé lenguajes usando el ejemplo de métodos básicos de procesamiento de imágenes: suma de imágenes, rotación, convolución, filtrado medio. Son estos métodos los que más a menudo deben escribirse en C ++. El tiempo de ejecución de la convolución es especialmente crítico.
Para cada uno de los métodos, excepto para el filtrado de la mediana, se realizaron tres implementaciones en C # y C ++:
Implementación ingenua usando métodos como GetPixel (x, y) y SetPixel (x, y, value);
Implementación optimizada usando punteros y trabajando con ellos a bajo nivel;
Implementación intrinsky (AVX).
(Array.Sort, std::sort), , , , . .
, , C# unmanaged - . - , C++ UB , C# - .
Github, , C#:
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void Sum_ThisProperty(NativeImage<float> img1, NativeImage<float> img2, NativeImage<float> res)
{
for (var j = 0; j < res.Height; j++)
for (var i = 0; i < res.Width; i++)
res[i, j] = img1[i, j] + img2[i, j];
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void Sum_Optimized(NativeImage<float> img1, NativeImage<float> img2, NativeImage<float> res)
{
var w = res.Width;
for (var j = 0; j < res.Height; j++)
{
var p1 = img1.PixelAddr(0, j);
var p2 = img2.PixelAddr(0, j);
var r = res.PixelAddr(0, j);
for (var i = 0; i < w; i++)
r[i] = p1[i] + p2[i];
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void Sum_Avx(NativeImage<float> img1, NativeImage<float> img2, NativeImage<float> res)
{
var w8 = res.Width / 8 * 8;
for (var j = 0; j < res.Height; j++)
{
var p1 = img1.PixelAddr(0, j);
var p2 = img2.PixelAddr(0, j);
var r = res.PixelAddr(0, j);
for (var i = 0; i < w8; i += 8)
{
Avx.StoreAligned(r, Avx.Add(Avx.LoadAlignedVector256(p1), Avx.LoadAlignedVector256(p2)));
p1 += 8;
p2 += 8;
r += 8;
}
for (var i = w8; i < res.Width; i++)
*r++ = *p1++ + *p2++;
}
}
. (1/10 ) 256x256 float 32 bit.
|
|
dotnet build -c Release |
g++ 10.2.0 -O0 |
g++ 10.2.0 -O1 |
g++ 10.2.0 -O2 |
g++ 10.2.0 -O3 |
clang 11.0.0 -O2 |
clang 11.0.0 -O3 |
Sum (naive) |
115.8 |
757.6 |
124.4 |
36.26 |
19.51 |
20.14 |
19.81 |
Sum (opt) |
40.69 |
255.6 |
36.07 |
24.48 |
19.60 |
20.11 |
19.81 |
Sum (avx) |
21.15 |
60.41 |
20.00 |
20.18 |
20.37 |
20.23 |
20.20 |
Rotate (naive) |
90.29 |
500.3 |
87.15 |
36.01 |
14.49 |
14.04 |
14.16 |
Rotate (opt) |
34.99 |
237.1 |
35.11 |
34.17 |
14.55 |
14.10 |
14.27 |
Rotate (avx) |
14.83 |
51.04 |
14.14 |
14.25 |
14.37 |
14.22 |
14.72 |
Median 3x3 |
4163 |
26660 |
2930 |
1607 |
2508 |
2301 |
2330 |
Median 5x5 |
11550 |
10090 |
8240 |
5554 |
5870 |
5610 |
6051 |
Median 7x7 |
23540 |
24470 |
17540 |
13640 |
12620 |
12920 |
13510 |
Convolve 7x7 (naive) |
5519 |
30900 |
3240 |
3694 |
2775 |
3047 |
2761 |
Convolve 7x7 (opt) |
2913 |
11780 |
2759 |
2628 |
2754 |
2434 |
2262 |
Convolve 7x7 (avx) |
709.2 |
3759 |
729.8 |
669.8 |
684.2 |
643.8 |
638.3 |
Convolve 7x7 (avx*) |
505.6 |
2984 |
523.4 |
511.5 |
507.8 |
443.2 |
443.3 |
: Convolve 7x7 (avx*) - , , .
Core i7-2600K @ 4.0 GHz.
:
(avx), C#, , C++. , C# !
C# , C# , C++ .
C# C++ 2 6 . .
Sí, puede escribir código computacional en C # que tenga paridad de rendimiento con C ++. Pero para hacer esto hay que recurrir a optimizaciones manuales en el código: lo que hace el compilador de C ++ automáticamente, en C # lo tienes que hacer tú mismo. Por lo tanto, si no tiene un enlace a C #, escriba más en C ++.
PD: Hay una característica excelente en .NET: la capacidad de generar código en tiempo de ejecución. Si el pipeline de procesamiento de imágenes no se conoce de antemano (por ejemplo, lo establece el usuario), entonces en C ++ tendrás que ensamblarlo a partir de ladrillos y, posiblemente, incluso usar funciones virtuales, mientras que en C # puedes lograr un mayor rendimiento simplemente generando un método.