深入分析java与C#底层控制能力区别及示例详解

目录

大家好,我是辣条。

刷到了一个很有意思的问题,Java和C#最大的不同是什么,辣条对Java和C#都没有研究的特别深,但是下面这个回答可供大家参考,同时欢迎大家在评论留下自己的看法。

我觉得抛开语法而谈,最主要的还是对底层的控制能力不同。

比如在 C# 里面你能干的:

var x = new int[10];
fixed (int* p = x)
{
    Console.WriteLine(*((long*)p - 1)); // 10
}

上述代码会输出 10,为什么?因为 .NET 中数组的长度存储于数组第一个元素之前的 8 字节内存中。如果你再接着输出 *((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 直接手动分配堆内存:

var array = (int*)NativeMemory.Alloc(10, sizeof(int));
array[0] = 1;
array[1] = 3;
Console.WriteLine(array[0] + array[1]); // 4
NativeMemory.Free(array);

上述调用等价于你在 C 语言中调用的 malloc,此外还有 AllocAlignedReallocAllocZeroed 等等,可以直接控制内存对齐。

接下来你想创建一个显式内存布局的结构 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,之所以会有上面的输出,是因为单精度浮点数 1 的二进制表示为 0x00111111100000000000000000000000,以小端方式存储后占 4 个字节,分别是 0x000000000x000000000x100000000x00111111

进一步,你还能直接从内存数据没有任何拷贝开销地构造对象:

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];
}

从堆内存创建自然也没问题:

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}{b}");
    return Marshal.StringToHGlobalAnsi(str);
}
var f = (delegate* unmanaged[Cdecl]<int, nint>)&Generate;
var result = Foo(f, 5);
Console.WriteLine(result); // wwwww

上面的代码干了什么事情?我们将 C# 的函数指针传到了 C++ 代码中,然后在 C++ 侧调用 C# 函数生成了一个字符串 wwwww,然后将这个字符串返回给 C# 侧。而就算不用函数指针换成使用委托也没有区别,因为 .NET 中的委托下面就是函数指针。

甚至,如果我们不想让 .NET 导入 foo.dll,我们想自行决定动态库的生命周期,还可以这么写:

[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) }), SuppressGCTransition]
static nint Generate(int count)
{
    var str = Enumerable.Repeat("w", count).Aggregate((a, b) => $"{a}{b}");
    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 上导入 .so.dylib 都完全不在话下。

再有,我们有一些数据想要进行计算,但是我们想使用 SIMD 进行处理,那只需要这么写:

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 自动消除。但其实除了手动编写 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;
}

因为现阶段当循环边界条件是向量长度时,.NET 会自动为我们做向量化并展开循环。

那么继续,我们还有refinout来做引用传递。

假设我们有一个很大的 struct,我们为了避免传递时发生拷贝,可以直接用 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 有专门的优化帮我们彻底消除掉内存分配,完全将 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 考虑是个热点路径,因此我加 MethodImplOptions.AggressiveInlining 来指导 JIT 有保证地内联此函数,最后为 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];
}

甚至还能搭配指针和手动分配内存来使用:

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 采用擦除,而是真真正正会对所有的类型参数特化代码(尽管对于引用类型会共享实现采用运行时分发),这也就意味着能最大程度确保性能,并且对应的类型拥有根据类型参数大小不同而特化的内存布局。还是上面那个 Point 的例子,我们将下面的数据 int 换成泛型参数 T,并做值类型数字的泛型约束:

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,生成的代码都非常优秀,不仅不存在任何的装箱拆箱,甚至没有任何的访存操作:

' 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

接着讲,我们有时候为了高性能想要临时暂停 GC 的回收,只需要简单的一句:

GC.TryStartNoGCRegion(1024 * 1024 * 128);

就能告诉 GC 如果还能分配 128mb 内存那就不要做回收了,然后一段时间内以后的代码我们尽管在这个预算内分配内存,任何 GC 都不会发生。甚至还能阻止在内存不够分配的情况下进行阻塞式 Full GC:

GC.TryStartNoGCRegion(1024 * 1024 * 128, true);

代码执行完了,最后的时候调用一句:

GC.EndNoGCRegion();

即可恢复 GC 行为。

除此之外,我们还能在运行时指定 GC 的模式来最大化性能:

GCSettings.LatencyMode = GCLatencyMode.Batch;
GCSettings.LatencyMode = GCLatencyMode.Interactive;
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
GCSettings.LatencyMode = GCLatencyMode.NoGCRegion;
GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;

更进一步,我们甚至可以直接将堆内存中的代码执行,在 .NET 上自己造一个 JIT,直接从内存创建一块可执行的区域然后往里面塞一段代码用来将两个32位整数相加:

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# 还有更多数不清的底层写法来和操作系统交互,甚至利用 C# 的编译器取消链接到自己的标准库,直接用从 0 开始造基础类型然后通过 NativeAOT 编译出完全无 GC、能够在裸机硬件上执行引导系统的 EFI 固件都是没有问题的。

另外还有 ILGPU 让你把 C# 代码直接跑在 GPU 上面,以及跑在嵌入式设备上直接操作 I2C、PWM、GPIO 等等,就不再举例子了。

而 C# 已经进了 roadmap 的后续更新内容:允许声明引用字段、添加表达固定长度内存的类型、允许传数组时消除数组分配、允许在栈上分配任何对象等等,无一不是在改进这些底层性能设施。

以上就是我认为的 C# 和 Java 最大的不同。

在 C# 中当你不需要上面这些的东西时,它们仿佛从来都不存在,允许动态类型、不断吸收各种函数式特性、还有各种语法糖加持,简洁度和灵活度甚至不输 Python,非常愉快和简单地就能编写各种代码;而一旦你需要,你可以拥有从上层到底层的几乎完全的控制能力,而这些能力将能让你有需要时无需思考各种奇怪的 workaround 就能直接榨干机器,达到 C、C++ 的性能,甚至因为有运行时 PGO 而超出 C、C++ 的性能。

以上就是深入分析java与C#底层控制能力不同的详细内容,更多关于java与C#底层控制能力不同分析的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java泛型类型通配符和C#对比分析

    c#的泛型没有类型通配符,原因是.net的泛型是CLR支持的泛型,而Java的JVM并不支持泛型,只是语法糖,在编译器编译的时候都转换成object类型 类型通配符在java中表示的是泛型类型的父类 public void test(List<Object> c) { for(int i = 0;i < c.size();i++) { System.out.println(c.get(i)); } } //创建一个List<String>对象 List<String&g

  • java与c#的区别、两者有什么不同?

    Java 的设计者是因为讨厌C++的复杂,于是Java 非常简洁,GC 也让内存管理非常方便,C# 是看中了Java 的GC,和虚拟机技术,希望把微软的几大语言集成到.NET 上来. 因此C#从语言上来讲并不简单甚至可以算的上复杂. 两种语言的设计思路也不一样,Java 是编译解释语言,C#是编译然后编译运行语言.Java 没有委托,C# 有委托.Java 倾向于用Interface 实现委托的功能,而 在C# 中,Abstract Class 比Interface 发挥了更大功能. Java

  • Java字符串操作和C#字符串操作的不同小结

    前言 每种语言都会有字符串的操作,因为字符串是我们平常开发使用频率最高的一种类型.今天我们来聊一下Java的字符串操作及在某些具体方法中与C#的不同,对于需要熟悉多种语言的人来说,作为一种参考.进行诫勉 首先,什么是字符串? 字符串是字符的序列,是作为一种对象而存在.说的直白点,字符串就是一些字符的组合,从而构成字符串,例如"abc"就是字符串,"郭志奇"也是一种赐福穿. 我们知道,Java是一种面向对象的高级程序语言.所有事物均为对象,字符串也不例外,也是一种对象

  • java与c#的语法区别详细介绍

    由C#转入Java一段时间了,总结下个人认为的Java同C#语法之间的不同之处,有不同意见之处还望各位海涵 刚学Java时觉得语法同C#大致是相同的(应该说C#同Java大致相同,毕竟人家微软的C#是有意模仿Java的语法习惯的) 比尔.盖茨曾经说过:"Java是最卓越的程序设计语言" 言归正传,下面探讨Java同C#的语法不同之处... 1,命名空间与包 C#为了把实现相似功能的类组织在一起,引入了命名空间的概念(namespace) Java中与此对应的东西叫做包(package)

  • 深入分析java与C#底层控制能力区别及示例详解

    目录 大家好,我是辣条. 刷到了一个很有意思的问题,Java和C#最大的不同是什么,辣条对Java和C#都没有研究的特别深,但是下面这个回答可供大家参考,同时欢迎大家在评论留下自己的看法. 我觉得抛开语法而谈,最主要的还是对底层的控制能力不同. 比如在 C# 里面你能干的: var x = new int[10]; fixed (int* p = x) { Console.WriteLine(*((long*)p - 1)); // 10 } 上述代码会输出 10,为什么?因为 .NET 中数组

  • Java结构型设计模式中代理模式示例详解

    目录 代理模式 分类 主要角色 作用 静态代理与动态代理的区别 静态代理的基本使用 创建抽象主题 创建真实主题 创建代理主题 客户端调用 JDK动态代理的基本使用 创建抽象主题 创建真实主题 创建代理主题 客户端调用 小优化 CGLIB动态代理的基本使用 创建抽象主题 创建真实主题 创建代理主题 客户端调用 小优化 CGLIB与JDK动态代理区别 1.执行条件 2.实现机制 3.性能 代理模式 代理模式(Proxy Pattern)属于结构型模式. 它是指为其他对象提供一种代理以控制对这个对象的

  • Java实战之实现物流配送系统示例详解

    目录 介绍 效果图展示 主要实现代码 介绍 系统分普通用户.企业.超级管理员等角色,除基础脚手架外,实现的功能有: 超级管理员:系统管理.用户管理.企业用户管理.普通用户管理.货物类型管理.车辆管理.公告管理.使用帮助等. 普通用户:注册登录.个人信息管理(个人资料.密码修改.充值.订单管理等).货物浏览.公告查看.下单等. 企业用户:注册登录.修改密码.充值.订单管理.货物管理.车辆管理.安排车辆等. 运行环境:windows/Linux均可.jdk1.8.mysql5.7.redis3.0.

  • Java框架设计灵魂之反射的示例详解

    目录 获取Class对象的方式 Class对象功能 获取成员变量们 获取构造方法们 获取成员方法们 获取全类名 Field:成员变量 Constructor:构造方法 Method:方法对象 案例 框架:半成品软件.可以在框架的基础上进行软件开发,简化编码. 反射就是把Java类中的各个成员映射成一个个的Java对象. 即在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法: 对于任意一个对象,都能调用它的任意一个方法和属性. 这种动态获取信息及动态调用对象方法的功能叫Java的反射机

  • java开发ShardingSphere的路由引擎类型示例详解

    目录 ShardingSphere的路由引擎类型 路由引擎类型 标准路由 路由逻辑 总结 ShardingSphere的路由引擎类型 本篇文章源码基于4.0.1版本 上篇文章我们了解到了ShardingSphere在路由流程过程中,根据不同类型的SQL会现在不同的路由引擎,而ShardingSphere支持的路由规则也很多了,包括广播(broadcast)路由.混合(complex)路由.默认数据库(defaultdb)路由.无效(ignore)路由.标准(standard)路由以及单播(uni

  • Go Java算法之外观数列实现方法示例详解

    目录 外观数列 方法一:遍历生成(Java) 方法二:递归(Go) 外观数列 给定一个正整数 n ,输出外观数列的第 n 项. 「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述. 你可以将其视作是由递归公式定义的数字字符串序列: countAndSay(1) = "1" countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串. 前五项如下: 1.1 —— 第一项是数字 1 2.11 —— 描述前一项,这个数

  • Go Java算法之Excel表列名称示例详解

    目录 Excel表列名称 方法一:数学(Java) 方法一:数学(Go) Excel表列名称 给你一个整数 columnNumber ,返回它在 Excel 表中相对应的列名称. 例如: A -> 1 B -> 2 C -> 3 ... Z -> 26 AA -> 27 AB -> 28 ... 示例 1: 输入:columnNumber = 1 输出:"A" 示例 2: 输入:columnNumber = 28 输出:"AB"

  • Go Java算法之二叉树的所有路径示例详解

    目录 二叉树的所有路径 方法一:深度优先遍历搜索(Java) 方法二:广度优先遍历(Go) 二叉树的所有路径 给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径. 叶子节点 是指没有子节点的节点. 示例 1: 输入:root = [1,2,3,null,5] 输出:["1->2->5","1->3"] 示例 2: 输入:root = [1] 输出:["1"] 提示: 树中节点的数目在范围 [1,

  • Java结构型设计模式中建造者模式示例详解

    目录 建造者模式 概述 角色 优缺点 应用场景 基本使用 创建产品类 创建建造者类 使用 链式写法 创建产品类与建造者类 使用 建造者模式 概述 建造者模式(Builder Pattern)属于创建型模式. 它是将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示. 简而言之:建造者模式就是使用多个简单的对象一步一步构建成一个复杂的对象. 建造者模式适用于创建对象需要很多步骤,但是步骤的顺序不一定固定.如果一个对象有非常复杂的内部结构(很多属性),可以将复杂对象的创建和使用进行分

  • Java中的Collections类的使用示例详解

    Collections的常用方法及其简单使用 代码如下: package Collections; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Stack; public class collections { public static void main(String[]args){ int array[]={125,75,56,7}; Li

随机推荐