Há uma opinião de que C # não tem lugar em tarefas computacionais, e essa opinião é bastante justificada: o compilador JIT é forçado a compilar e otimizar o código durante a execução do programa com atrasos mínimos, ele simplesmente não tem a oportunidade de gastar mais recursos computacionais para gerar um código mais eficiente , em contraste com o compilador C ++, que pode levar minutos e até horas neste assunto.
No entanto, nos últimos anos, a eficiência do compilador JIT aumentou visivelmente e vários chips úteis foram introduzidos na própria estrutura, por exemplo, intrínsecos .
E então me perguntei: é possível em 2020, usando .NET 5.0, escrever código que não seria muito inferior em desempenho ao C ++? Descobriu-se que você pode.
Motivação
Estou engajado no desenvolvimento de algoritmos de processamento de imagem e em um nível bastante baixo. Ou seja, não se trata de malabarismo com tijolos em Python, mas sim do desenvolvimento de algo novo e, de preferência, produtivo. O código Python leva um tempo inaceitavelmente longo, enquanto o uso de C ++ leva a uma diminuição na velocidade de desenvolvimento. O equilíbrio ideal entre produtividade e desempenho para tais tarefas é alcançado usando C # e Java. Confirmando minhas palavras - o projeto Fiji .
Anteriormente, usei C # para prototipagem e reescrevi algoritmos prontos que são essenciais para o desempenho em C ++, coloquei-os na biblioteca e retirei a biblioteca de C #. Mas, neste caso, a portabilidade era prejudicada e não era muito conveniente depurar o código.
Mas isso foi há muito tempo, desde então o .NET deu um passo à frente, e eu me pergunto se eu poderia abandonar a biblioteca nativa C ++ e mudar totalmente para C #?
Cenário
Vou comparar linguagens usando o exemplo de métodos básicos de processamento de imagens: soma de imagens, rotação, convolução, filtragem de mediana. São esses métodos que mais frequentemente precisam ser escritos em C ++. O tempo de execução da convolução é especialmente crítico.
Para cada um dos métodos, exceto para a filtragem de mediana, três implementações foram feitas em C # e C ++:
Implementação ingênua usando métodos como GetPixel (x, y) e SetPixel (x, y, value);
Implementação otimizada usando ponteiros e trabalhando com eles em um nível baixo;
Implementação 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 . .
, C# , C++. : , C++ , C# . C#, C++.
P.S. .NET - - . (, ), C++ , , , C# , .