Java 和C# 最大的不同是对底层的控制能力不同

再比如,此时你面前有一个使用 C++ 编写的库,其中有这么一段代码:

#include

#include

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 l, Vector128 r)

{

if (Avx2.IsSupported)

{

var result = Avx2.Multiply(vec1, vec2);

float sum = 0;

for (var i = 0; i < Vector128.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.Count; i++) sum += result.GetElement(i);

return sum;

}

else

{

float sum = 0;

for (int i = 0; i < Vector128.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 l, Vector128 r)

{

float sum = 0;

for (int i = 0; i < Vector128.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(x1, y1);

var p2 = new Point(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(x1, y1);

var p2 = new Point(x2, y2);

var result = GetDistanceSquare(p1, p2);

return Math.Sqrt(result);

}

[MethodImpl(MethodImplOptions.AggressiveInlining)]

T GetDistanceSquare(Point a, Point b) where T : struct, IBinaryNumber

{

return (a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y);

}

struct Point where T : struct, IBinaryNumber

{

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

总结

总的来说,面试是有套路的,一面基础,二面架构,三面个人。

最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友

三面蚂蚁金服成功拿到offer后,他说他累了

三面蚂蚁金服成功拿到offer后,他说他累了

没有任何的访存操作:

’ 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

总结

总的来说,面试是有套路的,一面基础,二面架构,三面个人。

最后,小编这里收集整理了一些资料,其中包括面试题(含答案)、书籍、视频等。希望也能帮助想进大厂的朋友

[外链图片转存中…(img-Q3C1pDf9-1714312586790)]

[外链图片转存中…(img-FvMasiNE-1714312586790)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值