هناك رأي مفاده أن 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 # يمكنك تحقيق أداء أكبر ببساطة عن طريق إنشاء طريقة.