最后
不知道你们用的什么环境,我一般都是用的Python3.6环境和pycharm解释器,没有软件,或者没有资料,没人解答问题,都可以免费领取(包括今天的代码),过几天我还会做个视频教程出来,有需要也可以领取~
给大家准备的学习资料包括但不限于:
Python 环境、pycharm编辑器/永久激活/翻译插件
python 零基础视频教程
Python 界面开发实战教程
Python 爬虫实战教程
Python 数据分析实战教程
python 游戏开发实战教程
Python 电子书100本
Python 学习路线规划
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
#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 会自动为我们做向量化并展开循环。
那么继续,我们还有ref
、in
、out
来做引用传递。
假设我们有一个很大的 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 的后续更新内容:允许声明引用字段、添加表达固定长度内存的类型、允许传数组时消除数组分配、允许在栈上分配任何对象等等,无一不是在改进这些底层性能设施。
感谢每一个认真阅读我文章的人,看着粉丝一路的上涨和关注,礼尚往来总是要有的:
① 2000多本Python电子书(主流和经典的书籍应该都有了)
② Python标准库资料(最全中文版)
③ 项目源码(四五十个有趣且经典的练手项目及源码)
④ Python基础入门、爬虫、web开发、大数据分析方面的视频(适合小白学习)
⑤ Python学习路线图(告别不入流的学习)
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!