مقارنة أداء C # و C ++ لمهام معالجة الصور

هناك رأي مفاده أن C # ليس لها مكان في المهام الحسابية ، وهذا الرأي معقول تمامًا: يُجبر مترجم JIT على تجميع الشفرة وتحسينها أثناء تنفيذ البرنامج بأقل تأخير ، وببساطة لا تتاح له الفرصة لإنفاق المزيد من الموارد الحسابية لإنشاء كود أكثر كفاءة على عكس مترجم C ++ ، والذي قد يستغرق دقائق وحتى ساعات في هذا الشأن.





ومع ذلك ، في السنوات الأخيرة ، زادت كفاءة مترجم JIT بشكل كبير ، وتم إدخال عدد من الرقائق المفيدة في الإطار نفسه ، على سبيل المثال ، الجوهر .





والآن كنت أتساءل: هل من الممكن في عام 2020 ، باستخدام .NET 5.0 ، كتابة تعليمات برمجية لن تكون أقل جودة من C ++ في الأداء؟ اتضح أنك تستطيع.





التحفيز

أنا منخرط في تطوير خوارزميات معالجة الصور ، وعلى مستوى منخفض إلى حد ما. وهذا يعني أن هذا لا يتلاعب بالطوب في بايثون ، ولكن تطوير شيء جديد ، ويفضل أن يكون منتجًا. تستغرق شفرة Python وقتًا طويلاً بشكل غير مقبول ، بينما يؤدي استخدام C ++ إلى انخفاض سرعة التطوير. يتم تحقيق التوازن الأمثل بين الإنتاجية والأداء لمثل هذه المهام باستخدام C # و Java. تأكيدا لكلامي - مشروع فيجي .





في السابق ، استخدمت C # للنماذج الأولية ، وأعدت كتابة الخوارزميات الجاهزة التي تعتبر بالغة الأهمية للأداء في C ++ ، وقمت بدفعها إلى lib وسحبت lib من C #. ولكن في هذه الحالة ، عانت قابلية النقل ، ولم يكن تصحيح أخطاء الكود أمرًا مناسبًا للغاية.





ولكن كان ذلك منذ وقت طويل ، ومنذ ذلك الحين ، خطت .NET خطوات كبيرة للأمام ، وتساءلت عما إذا كان بإمكاني التخلي عن مكتبة C ++ الأصلية والتحول بالكامل إلى C #؟





سيناريو

سأقارن اللغات باستخدام مثال طرق معالجة الصور الأساسية: مجموع الصور ، الدوران ، الالتفاف ، التصفية المتوسطة. هذه هي الطرق التي يجب كتابتها في الغالب بلغة C ++. وقت تشغيل الالتواء أمر بالغ الأهمية بشكل خاص.





لكل طريقة من الطرق ، باستثناء الترشيح المتوسط ​​، تم إجراء ثلاثة تطبيقات في C # و C ++:





  • تنفيذ بسيط باستخدام طرق مثل GetPixel (x ، y) و SetPixel (x ، y ، value) ؛





  • التنفيذ الأمثل باستخدام المؤشرات والعمل معها على مستوى منخفض ؛





  • تطبيق 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 ++.





ملاحظة: هناك ميزة واحدة قاتلة في .NET - القدرة على إنشاء رمز في وقت التشغيل. إذا لم يكن خط أنابيب معالجة الصور معروفًا مسبقًا (على سبيل المثال ، تم تعيينه من قبل المستخدم) ، فسيتعين عليك في C ++ تجميعه من الطوب ، وربما حتى استخدام الوظائف الافتراضية ، بينما في C # يمكنك تحقيق أداء أكبر ببساطة عن طريق إنشاء طريقة.












All Articles