C#は計算タスクに適さないという意見があり、この意見は非常に正当化されます。JITコンパイラは、プログラムの実行中に最小限の遅延でコードをオンザフライでコンパイルおよび最適化することを余儀なくされ、より効率的なコードを生成するためにより多くの計算リソースを費やす機会がないだけです。 、この問題で数分から数時間かかる可能性があるC ++コンパイラとは対照的です。
ただし、近年、JITコンパイラの効率が著しく向上し、組み込みなどの多くの有用なチップがフレームワーク自体に組み込まれています。
そして今、それは私にとって興味深いものになりました。2020年に.NET 5.0を使用して、C ++よりもパフォーマンスがそれほど劣らないコードを書くことは可能ですか?できることがわかった。
動機
私は画像処理アルゴリズムの開発に携わっていますが、かなり低いレベルです。つまり、これはPythonのブリックとのやりとりではなく、新しい、できれば生産的なものの開発です。Pythonコードは許容できないほど長い時間がかかりますが、C ++を使用すると開発速度が低下します。このようなタスクの生産性とパフォーマンスの最適なバランスは、C#とJavaを使用して実現されます。私の言葉を確認して-フィジープロジェクト。
以前は、プロトタイピングにC#を使用し、C ++でのパフォーマンスに不可欠な既製のアルゴリズムを書き直して、それらをlibに押し込み、C#からlibをプルしました。しかし、この場合、移植性が損なわれ、コードをデバッグするのはあまり便利ではありませんでした。
しかし、それはかなり前のことでした。それ以来、.NETは大きく前進し、ネイティブのC ++ライブラリを放棄して完全にC#に切り替えることができるかどうか疑問に思いました。
シナリオ
基本的な画像処理方法の例を使用して言語を比較します:画像の合計、回転、畳み込み、中央値フィルタリング。ほとんどの場合、C ++で記述しなければならないのはこれらのメソッドです。コンボリューションの実行時間は特に重要です。
中央値フィルタリングを除く各メソッドについて、C#およびC ++で3つの実装が行われました。
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 ++でさらに記述します。
PS .NETには、1つのキラー機能があります。それは、実行時にコードを生成する機能です。画像処理パイプラインが事前にわからない場合(たとえば、ユーザーが設定する場合)、C ++では、レンガから組み立てる必要があり、場合によっては仮想関数を使用する必要がありますが、C#では、メソッドを生成するだけでパフォーマンスを向上させることができます。