Comparaison des performances C # et C ++ dans les tâches de traitement d'image

Il y a une opinion que C # n'a pas sa place dans les tâches de calcul, et cette opinion est tout à fait raisonnable: le compilateur JIT est obligé de compiler et d'optimiser le code à la volée pendant l'exécution du programme avec des retards minimes, il n'a tout simplement pas la possibilité de dépenser plus de ressources de calcul pour générer du code plus efficace , contrairement au compilateur C ++, qui peut prendre des minutes et même des heures à ce sujet.





Cependant, ces dernières années, l'efficacité du compilateur JIT a considérablement augmenté et un certain nombre de puces utiles ont été introduites dans le cadre lui-même, par exemple, des composants intrinsèques .





Et puis je me suis demandé: est-il possible en 2020, en utilisant .NET 5.0, d'écrire du code dont les performances ne seraient pas très inférieures à C ++? Il s'est avéré que vous pouvez.





Motivation

Je suis engagé dans le développement d'algorithmes de traitement d'image, et à un niveau assez bas. Autrement dit, il ne s'agit pas de jongler avec des briques en Python, mais de développer quelque chose de nouveau et, de préférence, productif. Le code Python prend un temps inacceptable, tandis que l'utilisation de C ++ entraîne une diminution de la vitesse de développement. L'équilibre optimal entre productivité et performances pour de telles tâches est obtenu en utilisant C # et Java. En confirmation de mes paroles - le projet des Fidji .





Auparavant, j'utilisais C # pour le prototypage, et j'ai réécrit des algorithmes prêts à l'emploi qui sont essentiels pour les performances en C ++, les ai poussés dans la bibliothèque et j'ai extrait la bibliothèque de C #. Mais dans ce cas, la portabilité a souffert et il n'était pas très pratique de déboguer le code.





, .NET , , C++ C#?





: , , , . C++. .





, , C# C++:





  • GetPixel(x, y) SetPixel(x, y, value);





  • ;





  • (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 . .





Oui, vous pouvez écrire du code de calcul en C # avec une parité de performances avec C ++. Mais pour ce faire, vous devez recourir à des optimisations manuelles dans le code: ce que le compilateur C ++ fait automatiquement, en C #, vous devez le faire vous-même. Par conséquent, si vous n'avez pas de liaison à C #, écrivez davantage en C ++.





PS Il y a une fonctionnalité qui tue dans .NET - la possibilité de générer du code à l'exécution. Si le pipeline de traitement d'image n'est pas connu à l'avance (par exemple, il est défini par l'utilisateur), alors en C ++, vous devrez l'assembler à partir de briques et, éventuellement, même utiliser des fonctions virtuelles, tandis qu'en C #, vous pouvez obtenir de meilleures performances simplement en générant une méthode.












All Articles