Comparando o desempenho de C # e C ++ para tarefas de processamento de imagem

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# , .












All Articles