Es gibt eine Meinung, dass C # keinen Platz in Rechenaufgaben hat, und diese Meinung ist durchaus vernünftig: Der JIT-Compiler ist gezwungen, den Code während der Programmausführung mit minimalen Verzögerungen im laufenden Betrieb zu kompilieren und zu optimieren. Er hat einfach keine Möglichkeit, mehr Rechenressourcen auszugeben, um effizienteren Code zu generieren im Gegensatz zum C ++ - Compiler, der in dieser Angelegenheit Minuten und sogar Stunden dauern kann.
In den letzten Jahren hat sich die Effizienz des JIT-Compilers jedoch erheblich erhöht, und eine Reihe nützlicher Chips wurden in das Framework selbst integriert, z. B. Intrinsics .
Und jetzt habe ich mich gefragt: Ist es im Jahr 2020 möglich, mit .NET 5.0 Code zu schreiben, dessen Leistung C ++ nicht wesentlich unterlegen wäre? Es stellte sich heraus, dass Sie können.
Motivation
Ich beschäftige mich mit der Entwicklung von Bildverarbeitungsalgorithmen auf einem relativ niedrigen Niveau. Das heißt, dies ist kein Jonglieren mit Steinen in Python, sondern die Entwicklung von etwas Neuem und vorzugsweise Produktivem. Python-Code dauert unannehmbar lange, während die Verwendung von C ++ zu einer Verringerung der Entwicklungsgeschwindigkeit führt. Das optimale Gleichgewicht zwischen Produktivität und Leistung für solche Aufgaben wird mit C # und Java erreicht. Zur Bestätigung meiner Worte - das Fidschi- Projekt .
Zuvor habe ich C # für das Prototyping verwendet und vorgefertigte Algorithmen, die für die Leistung in C ++ von entscheidender Bedeutung sind, neu geschrieben, in die Bibliothek verschoben und die Bibliothek aus C # gezogen. In diesem Fall litt die Portabilität, und es war nicht sehr praktisch, den Code zu debuggen.
, .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 . .
Ja, Sie können Rechencode in C # schreiben, der eine Leistungsparität mit C ++ aufweist. Dazu müssen Sie jedoch auf manuelle Optimierungen im Code zurückgreifen: Was der C ++ - Compiler automatisch tut, müssen Sie in C # selbst tun. Wenn Sie keine Bindung zu C # haben, schreiben Sie daher weiter in C ++.
PS .NET verfügt über eine Killer-Funktion: Es ist die Möglichkeit, zur Laufzeit Code zu generieren. Wenn die Bildverarbeitungspipeline nicht im Voraus bekannt ist (z. B. vom Benutzer festgelegt), müssen Sie sie in C ++ aus Bausteinen zusammensetzen und möglicherweise sogar virtuelle Funktionen verwenden, während Sie in C # eine höhere Leistung erzielen können, indem Sie einfach eine Methode generieren.