深入分析java與C#底層控制能力區(qū)別及示例詳解
大家好,我是辣條。
刷到了一個很有意思的問題,Java和C#最大的不同是什么,辣條對Java和C#都沒有研究的特別深,但是下面這個回答可供大家參考,同時歡迎大家在評論留下自己的看法。
我覺得拋開語法而談,最主要的還是對底層的控制能力不同。
比如在 C# 里面你能干的
var x = new int[10]; fixed (int* p = x) { Console.WriteLine(*((long*)p - 1)); // 10 }
上述代碼會輸出 10,為什么?因?yàn)?.NET 中數(shù)組的長度存儲于數(shù)組第一個元素之前的 8 字節(jié)內(nèi)存中。如果你再接著輸出 *((long*)p - 2)
,將會直接得到這個對象的 TypeHandle
地址:
Console.WriteLine((long)typeof(int[]).TypeHandle.Value == *((long*)p - 2)); // True
然后拿著這個指針又接著能去訪問對象的 MethodTable
。
再有你還可以手動在棧上分配空間
var x = stackalloc int[2]; // 或者 Span<int> x = stackalloc int[2]; 做安全訪存 x[0] = 3; x[1] = 1; Console.WriteLine(x[0] + x[1]); // 4
接著你想繞過 GC 直接手動分配堆內(nèi)存
var array = (int*)NativeMemory.Alloc(10, sizeof(int)); array[0] = 1; array[1] = 3; Console.WriteLine(array[0] + array[1]); // 4 NativeMemory.Free(array);
上述調(diào)用等價于你在 C 語言中調(diào)用的 malloc
,此外還有 AllocAligned
、Realloc
、AllocZeroed
等等,可以直接控制內(nèi)存對齊。
接下來你想創(chuàng)建一個顯式內(nèi)存布局的結(jié)構(gòu) Foo
var obj = new Foo(); obj.Float = 1; Console.WriteLine(obj.Int); // 1065353216 Console.WriteLine(obj.Bytes[0]); // 0 Console.WriteLine(obj.Bytes[1]); // 0 Console.WriteLine(obj.Bytes[2]); // 128 Console.WriteLine(obj.Bytes[3]); // 63 [StructLayout(LayoutKind.Explicit)] struct Foo { [FieldOffset(0)] public int Int; [FieldOffset(0)] public float Float; [FieldOffset(0)] public unsafe fixed byte Bytes[4]; }
然后你就成功模擬出了一個 C 的 Union,之所以會有上面的輸出,是因?yàn)閱尉雀↑c(diǎn)數(shù) 1 的二進(jìn)制表示為 0x00111111100000000000000000000000
,以小端方式存儲后占 4 個字節(jié),分別是 0x00000000
、0x00000000
、0x10000000
、0x00111111
。
進(jìn)一步,你還能直接從內(nèi)存數(shù)據(jù)沒有任何拷貝開銷地構(gòu)造對象:
var data = stackalloc byte[] { 0, 0, 128, 63 }; var foo = Unsafe.AsRef<Foo>(data); Console.WriteLine(foo.Float); // 1 [StructLayout(LayoutKind.Explicit)] struct Foo { [FieldOffset(0)] public int Int; [FieldOffset(0)] public float Float; [FieldOffset(0)] public unsafe fixed byte Bytes[4]; }
甚至這樣:
var data = 1065353216; var foo = Unsafe.AsRef<Foo>(&data); Console.WriteLine(foo.Float); // 1 [StructLayout(LayoutKind.Explicit)] struct Foo { [FieldOffset(0)] public int Int; [FieldOffset(0)] public float Float; [FieldOffset(0)] public unsafe fixed byte Bytes[4]; }
從堆內(nèi)存創(chuàng)建自然也沒問題
var data = new byte[] { 0, 0, 128, 63 }; fixed (void* p = data) { var foo = Unsafe.AsRef<Foo>(p); Console.WriteLine(foo.Float); // 1 } [StructLayout(LayoutKind.Explicit)] struct Foo { [FieldOffset(0)] public int Int; [FieldOffset(0)] public float Float; [FieldOffset(0)] public unsafe fixed byte Bytes[4]; }
再比如,此時你面前有一個使用 C++ 編寫的庫,其中有這么一段代碼:
#include <cstring> #include <cstdio> extern "C" __declspec(dllexport) char* __cdecl foo(char* (*gen)(int), int count) { return gen(count); }
然后我們編寫如下 C# 代碼:
[DllImport("./foo.dll", EntryPoint = "foo"), SuppressGCTransition] static extern string Foo(delegate* unmanaged[Cdecl]<int, nint> gen, int count); [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) }), SuppressGCTransition] static nint Generate(int count) { var str = Enumerable.Repeat("w", count).Aggregate((a, b) => $"{a}"); return Marshal.StringToHGlobalAnsi(str); } var f = (delegate* unmanaged[Cdecl]<int, nint>)&Generate; var result = Foo(f, 5); Console.WriteLine(result); // wwwww
上面的代碼干了什么事情?我們將 C# 的函數(shù)指針傳到了 C++ 代碼中,然后在 C++ 側(cè)調(diào)用 C# 函數(shù)生成了一個字符串 wwwww
,然后將這個字符串返回給 C# 側(cè)。而就算不用函數(shù)指針換成使用委托也沒有區(qū)別,因?yàn)?.NET 中的委托下面就是函數(shù)指針。
甚至,如果我們不想讓 .NET 導(dǎo)入 foo.dll
,
我們想自行決定動態(tài)庫的生命周期
還可以這么寫:
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) }), SuppressGCTransition] static nint Generate(int count) { var str = Enumerable.Repeat("w", count).Aggregate((a, b) => $"{a}"); return Marshal.StringToHGlobalAnsi(str); } var f = (delegate* unmanaged[Cdecl]<int, nint>)&Generate; var library = NativeLibrary.Load("./foo.dll"); var foo = (delegate* unmanaged[Cdecl, SuppressGCTransition]<delegate* unmanaged[Cdecl]<int, nint>, int, string>)NativeLibrary.GetExport(library, "foo"); var result = foo(f, 5); Console.WriteLine(result); // wwwww NativeLibrary.Free(library);
上面這些都不是 Windows 專用,在 Linux、macOS 上導(dǎo)入 .so
和 .dylib
都完全不在話下。
再有,我們有一些數(shù)據(jù)想要進(jìn)行計算,但是我們想使用 SIMD 進(jìn)行處理,那只需要這么寫:
var vec1 = Vector128.Create(1.1f, 2.2f, 3.3f, 4.4f); var vec2 = Vector128.Create(5.5f, 6.6f, 7.7f, 8.8f); Console.WriteLine(Calc(vec1, vec2)); float Calc(Vector128<float> l, Vector128<float> r) { if (Avx2.IsSupported) { var result = Avx2.Multiply(vec1, vec2); float sum = 0; for (var i = 0; i < Vector128<float>.Count; i++) sum += result.GetElement(i); return sum; } else if (Rdm.IsSupported) { var result = Rdm.Multiply(vec1, vec2); float sum = 0; for (var i = 0; i < Vector128<float>.Count; i++) sum += result.GetElement(i); return sum; } else { float sum = 0; for (int i = 0; i < Vector128<float>.Count; i++) { sum += l.GetElement(i) * r.GetElement(i); } return sum; } }
可以看看在 X86 平臺上生成了什么代碼:
vzeroupper vmovupd xmm0, [r8] vmulps xmm0, xmm0, [r8+0x10] vmovaps xmm1, xmm0 vxorps xmm2, xmm2, xmm2 vaddss xmm1, xmm1, xmm2 vmovshdup xmm2, xmm0 vaddss xmm1, xmm2, xmm1 vunpckhps xmm2, xmm0, xmm0 vaddss xmm1, xmm2, xmm1 vshufps xmm0, xmm0, xmm0, 0xff vaddss xmm1, xmm0, xmm1 vmovaps xmm0, xmm1 ret
平臺判斷的分支會被 JIT 自動消除。但其實(shí)除了手動編寫 SIMD 代碼之外,前兩個分支完全可以不寫,而只留下:
float Calc(Vector128<float> l, Vector128<float> r) { float sum = 0; for (int i = 0; i < Vector128<float>.Count; i++) { sum += l.GetElement(i) * r.GetElement(i); } return sum; }
因?yàn)楝F(xiàn)階段當(dāng)循環(huán)邊界條件是向量長度時,.NET 會自動為我們做向量化并展開循環(huán)。
那么繼續(xù),我們還有ref
、in
、out
來做引用傳遞。
假設(shè)我們有一個很大的 struct
,我們?yōu)榱吮苊鈧鬟f時發(fā)生拷貝,可以直接用 in
來做只讀引用傳遞:
void Test(in Foo v) { } struct Foo { public long A, B, C, D, E, F, G, H, I, J, K, L, M, N; }
而對于小的 struct
,.NET 有專門的優(yōu)化幫我們徹底消除掉內(nèi)存分配,完全將 struct
放在寄存器中,例如如下代碼:
double Test(int x1, int y1, int x2, int y2) { var p1 = new Point(x1, y1); var p2 = new Point(x2, y2); return GetDistance(p1, p2); } [MethodImpl(MethodImplOptions.AggressiveInlining)] double GetDistance(Point a, Point b) { return Math.Sqrt((a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y)); } struct Point { public Point(int x, int y) { X = x; Y = y; } public int X { get; set; } public int Y { get; set; } }
上述代碼 GetDistance
考慮是個熱點(diǎn)路徑,因此我加 MethodImplOptions.AggressiveInlining
來指導(dǎo) JIT 有保證地內(nèi)聯(lián)此函數(shù),最后為 Test
生成了如下的代碼:
vzeroupper sub ecx, r8d mov eax, ecx imul eax, ecx sub edx, r9d mov ecx, edx imul edx, ecx add eax, edx vxorps xmm0, xmm0, xmm0 vcvtsi2sd xmm0, xmm0, eax vsqrtsd xmm0, xmm0, xmm0 ret
全程沒有一句指令訪存,非常的高效。
我們還可以借用 ref 的引用語義來做原地更新
var vec = new Vector(10); vec[2] = 5; Console.WriteLine(vec[2]); // 5 ref var x = ref vec[3]; x = 7; Console.WriteLine(vec[3]); // 7 class Vector { private int[] _array; public Vector(int count) => _array = new int[count]; public ref int this[int index] => ref _array[index]; }
甚至還能搭配指針和手動分配內(nèi)存來使用
var vec = new Vector(10); vec[2] = 5; Console.WriteLine(vec[2]); // 5 ref var x = ref vec[3]; x = 7; Console.WriteLine(vec[3]); // 7 unsafe class Vector { private int* _memory; public Vector(uint count) => _memory = (int*)NativeMemory.Alloc(count, sizeof(int)); public ref int this[int index] => ref _memory[index]; ~Vector() => NativeMemory.Free(_memory); }
C# 的泛型不像 Java 采用擦除,而是真真正正會對所有的類型參數(shù)特化代碼(盡管對于引用類型會共享實(shí)現(xiàn)采用運(yùn)行時分發(fā)),這也就意味著能最大程度確保性能,并且對應(yīng)的類型擁有根據(jù)類型參數(shù)大小不同而特化的內(nèi)存布局。還是上面那個 Point
的例子,我們將下面的數(shù)據(jù) int
換成泛型參數(shù) T
,并做值類型數(shù)字的泛型約束:
double Test1(double x1, double y1, double x2, double y2) { var p1 = new Point<double>(x1, y1); var p2 = new Point<double>(x2, y2); var result = GetDistanceSquare(p1, p2); return Math.Sqrt(result); } double Test2(int x1, int y1, int x2, int y2) { var p1 = new Point<int>(x1, y1); var p2 = new Point<int>(x2, y2); var result = GetDistanceSquare(p1, p2); return Math.Sqrt(result); } [MethodImpl(MethodImplOptions.AggressiveInlining)] T GetDistanceSquare<T>(Point<T> a, Point<T> b) where T : struct, IBinaryNumber<T> { return (a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y); } struct Point<T> where T : struct, IBinaryNumber<T> { public Point(T x, T y) { X = x; Y = y; } public T X { get; set; } public T Y { get; set; } }
無論是 Test1
還是 Test2
,生成的代碼都非常優(yōu)秀,不僅不存在任何的裝箱拆箱,甚至沒有任何的訪存操作:
' Test1 vzeroupper vsubsd xmm0, xmm0, xmm2 vmovaps xmm2, xmm0 vmulsd xmm0, xmm0, xmm2 vsubsd xmm1, xmm1, xmm3 vmovaps xmm2, xmm1 vmulsd xmm1, xmm1, xmm2 vaddsd xmm0, xmm1, xmm0 vsqrtsd xmm0, xmm0, xmm0 ret ' Test2 vzeroupper sub ecx, r8d mov eax, ecx imul eax, ecx sub edx, r9d mov ecx, edx imul edx, ecx add eax, edx vxorps xmm0, xmm0, xmm0 vcvtsi2sd xmm0, xmm0, eax vsqrtsd xmm0, xmm0, xmm0 ret
接著講,我們有時候?yàn)榱烁咝阅芟胍R時暫停 GC 的回收,只需要簡單的一句:
GC.TryStartNoGCRegion(1024 * 1024 * 128);
就能告訴 GC 如果還能分配 128mb 內(nèi)存那就不要做回收了,然后一段時間內(nèi)以后的代碼我們盡管在這個預(yù)算內(nèi)分配內(nèi)存,任何 GC 都不會發(fā)生。甚至還能阻止在內(nèi)存不夠分配的情況下進(jìn)行阻塞式 Full GC:
GC.TryStartNoGCRegion(1024 * 1024 * 128, true);
代碼執(zhí)行完了,最后的時候調(diào)用一句:
GC.EndNoGCRegion();
即可恢復(fù) GC 行為。
除此之外,我們還能在運(yùn)行時指定 GC 的模式來最大化性能:
GCSettings.LatencyMode = GCLatencyMode.Batch; GCSettings.LatencyMode = GCLatencyMode.Interactive; GCSettings.LatencyMode = GCLatencyMode.LowLatency; GCSettings.LatencyMode = GCLatencyMode.NoGCRegion; GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
更進(jìn)一步,我們甚至可以直接將堆內(nèi)存中的代碼執(zhí)行,在 .NET 上自己造一個 JIT,直接從內(nèi)存創(chuàng)建一塊可執(zhí)行的區(qū)域然后往里面塞一段代碼用來將兩個32位整數(shù)相加:
var kernel32 = NativeLibrary.Load("kernel32.dll"); var virtualProtectEx = (delegate* unmanaged[Cdecl, SuppressGCTransition]<nint, void*, nint, int, out int, bool>)NativeLibrary.GetExport(kernel32, "VirtualProtectEx"); var processHandle = Process.GetCurrentProcess().Handle; Memory<byte> code = new byte[] { 0x8d, 0x04, 0x11, // lea rax, [rcx+rdx] 0xc3 // ret } using (var handle = code.Pin()) { virtualProtectEx(processHandle, handle.Pointer, code.Length, 0x40, out _); var f = (delegate*<int, int, int>)handle.Pointer; Console.WriteLine(f(2, 3)); // 5 } virtualProtectEx = null; NativeLibrary.Free(kernel32);
除此之外,C# 還有更多數(shù)不清的底層寫法來和操作系統(tǒng)交互,甚至利用 C# 的編譯器取消鏈接到自己的標(biāo)準(zhǔn)庫,直接用從 0 開始造基礎(chǔ)類型然后通過 NativeAOT 編譯出完全無 GC、能夠在裸機(jī)硬件上執(zhí)行引導(dǎo)系統(tǒng)的 EFI 固件都是沒有問題的。
另外還有 ILGPU 讓你把 C# 代碼直接跑在 GPU 上面,以及跑在嵌入式設(shè)備上直接操作 I2C、PWM、GPIO 等等,就不再舉例子了。
而 C# 已經(jīng)進(jìn)了 roadmap 的后續(xù)更新內(nèi)容:允許聲明引用字段、添加表達(dá)固定長度內(nèi)存的類型、允許傳數(shù)組時消除數(shù)組分配、允許在棧上分配任何對象等等,無一不是在改進(jìn)這些底層性能設(shè)施。
以上就是我認(rèn)為的 C# 和 Java 最大的不同。
在 C# 中當(dāng)你不需要上面這些的東西時,它們仿佛從來都不存在,允許動態(tài)類型、不斷吸收各種函數(shù)式特性、還有各種語法糖加持,簡潔度和靈活度甚至不輸 Python,非常愉快和簡單地就能編寫各種代碼;而一旦你需要,你可以擁有從上層到底層的幾乎完全的控制能力,而這些能力將能讓你有需要時無需思考各種奇怪的 workaround 就能直接榨干機(jī)器,達(dá)到 C、C++ 的性能,甚至因?yàn)橛羞\(yùn)行時 PGO 而超出 C、C++ 的性能。
以上就是深入分析java與C#底層控制能力不同的詳細(xì)內(nèi)容,更多關(guān)于java與C#底層控制能力不同分析的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談服務(wù)發(fā)現(xiàn)和負(fù)載均衡的來龍去脈
單機(jī)時代,傳統(tǒng)軟件大多是單體/巨石架構(gòu)(Monolithic)。大家往一個代碼倉庫提交CODE,這會導(dǎo)致應(yīng)用膨脹,以及擴(kuò)展受限,無法按需伸縮等諸多問題。單體架構(gòu)怎么解決多人合作的問題?模塊化,按功能拆分,模塊之間定義編程接口(API)。本篇文章帶你詳細(xì)了解。2021-05-05Scala函數(shù)式編程專題--scala基礎(chǔ)語法介紹
這篇文章主要介紹了scala基礎(chǔ)語法的的相關(guān)資料,文中講解非常詳細(xì),幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-06-06微信支付jsapi缺少參數(shù) total_fee 錯誤分析與解決方法
這篇文章主要介紹了微信支付jsapi缺少參數(shù) total_fee 錯誤分析與解決方法,需要的朋友可以參考下2018-03-03Hbuilder連遠(yuǎn)程接服務(wù)器上傳代碼的圖文教程
下面小編就為大家分享一篇Hbuilder連遠(yuǎn)程接服務(wù)器上傳代碼的圖文教程,具有很好的參考價值,一起跟隨小編過來看看吧,希望對大家有所幫助2017-11-11bilibili彈幕轉(zhuǎn)ass程序制作思路及過程
本文主要是為了方便線下播放Bilibili的彈幕,而專門制作的一款將彈幕轉(zhuǎn)換為ASS的程序,介紹了程序制作的思路及過程,有需要的朋友可以參考下2014-09-09DLL(Dynamic Linkable Library) 詳解說明
DLL文件(Dynamic Linkable Library 即動態(tài)鏈接庫文件),是一種不能單獨(dú)運(yùn)行的文件,它允許程序共享執(zhí)行特殊任務(wù)所必需的代碼和其他資源2008-12-12