使用.NET Half结构体优化卷积神经网络反而变慢的原因排查
使用.NET Half结构体改造CNN后性能下降的问题分析
我看到有程序优化建议提到,在无需高精度的场景下可尝试使用16位浮点数。虽该建议并非针对C#,但我仍尝试在.NET中使用新增的Half结构体对自己实现的卷积神经网络进行改造。然而修改后程序运行时长从30秒大幅增至50秒,尤其是卷积层的反向传播环节速度明显下降。以下是导致性能变慢的核心代码及相关结构体定义:
核心慢代码
protected void Backwards(FeatureMap input, Color[,] kernal, Color[,] dL_dK, FeatureMap dL_dP, FeatureMap dL_dPNext) { for (int strideX = 0; strideX < dL_dP.Width; strideX++) { for (int strideY = 0; strideY < dL_dP.Length; strideY++) { for (int kernalX = 0; kernalX < _kernalSize; kernalX++) { for (int kernalY = 0; kernalY < _kernalSize; kernalY++) { int x = strideX * _stride + kernalX; int y = strideY * _stride + kernalY; Color dK = dL_dP[strideX, strideY] * input[x, y] * _invK2; Color dP = dL_dP[strideX, strideX] * kernal[kernalX, kernalY] * _invK2; dL_dK[kernalX, kernalY] += dK; dL_dPNext[x, y] += dP; } } } } for (int i = 0; i < _kernalSize; i++) { for (int j = 0; j < _kernalSize; j++) { dL_dK[i, j] = dL_dK[i, j].Clamp(CLAMP); } } for (int i = 0; i < dL_dPNext.Width; i++) { for (int j = 0; j < dL_dPNext.Length; j++) { dL_dPNext[i, j] = dL_dPNext[i, j].Clamp(Half.One); } } }
Color结构体定义
public readonly struct Color { public Color(Half r, Half g, Half b) { R = r; G = g; B = b; } public Half B { get; } public Half G { get; } public Half Magnitude => Half.Sqrt(R * R + G * G + B * B); public Half SquareMagnitude => R * R + G * G + B * B; public Half R { get; } public static Color operator -(Color color1, Color color2) { return new Color(color1.R - color2.R, color1.G - color2.G, color1.B - color2.B); } public static Color operator -(Color color) { return new Color(-color.R, -color.G, -color.B); } public static Color operator *(Color color1, Color color2) { return new Color(color1.R * color2.R, color1.G * color2.G, color1.B * color2.B); } public static Color operator *(Color color, Half multiple) { return new Color(color.R * multiple, color.G * multiple, color.B * multiple); } public static Color operator *(Half multiple, Color color) { return new Color(color.R * multiple, color.G * multiple, color.B * multiple); } public static Color operator /(Color color1, Color color2) { return new Color(color1.R / color2.R, color1.G / color2.G, color1.B / color2.B); } public static Color operator +(Color color1, Color color2) { return new Color(color1.R + color2.R, color1.G + color2.G, color1.B + color2.B); } public Color Clamp(Half val) { return new Color(R > val ? val : R < -val ? -val : R, G > val ? val : G < -val ? -val : G, B > val ? val : B < -val ? -val : B); } }
注:FeatureMap是Color类型的二维数组。
问题原因分析
- CPU对Half的硬件支持不足:x86/64架构的CPU没有原生的16位浮点运算单元,所有Half类型的运算都需要先转换为32位float计算,再转回Half。在你的四重循环这种高频运算场景下,频繁的类型转换会带来巨大的额外开销,直接拖慢速度。
- readonly结构体的频繁实例化:你的Color是readonly struct,每次执行加减乘除运算都会创建新的Color实例。循环中反复创建值类型实例(即使在栈上)也会增加运算指令数,放大性能损耗。
- 循环顺序导致缓存命中率低:当前循环顺序是
strideX → strideY → kernalX → kernalY,这种顺序会让input、dL_dPNext等二维数组的内存访问不连续。CPU缓存无法有效命中,频繁的内存读写进一步降低了运算效率——Half的内存优势完全被缓存命中率下降抵消。
优化方案
1. 减少Half的类型转换开销
- 提前将
_invK2这类常量转换为Half类型,避免每次运算时的隐式转换:private readonly Half _invK2Half; // 初始化时赋值 _invK2Half = (Half)_invK2; - 优先使用.NET 6+提供的
Vector<Half>进行向量化运算,利用CPU的SIMD指令集批量处理数据,减少转换次数。
2. 优化Color结构体的运算逻辑
- 移除readonly修饰符(如果业务允许),或者添加原地修改的方法,避免频繁创建新实例:
public void AddInPlace(Color other) { // 注意:此时结构体不能是readonly,字段需改为可写 R += other.R; G += other.G; B += other.B; } - 合并运算步骤,减少中间实例:比如将
dL_dP[strideX, strideY] * input[x, y] * _invK2Half拆分为单个通道的运算,直接赋值到临时变量,再组装成Color,减少一次乘法运算的实例创建。
3. 调整循环顺序提升缓存命中率
根据C#二维数组的行优先内存布局,将循环顺序调整为kernalX → kernalY → strideX → strideY,让数组访问更连续,充分利用CPU的L1/L2缓存:
// 调整后的循环顺序 for (int kernalX = 0; kernalX < _kernalSize; kernalX++) { for (int kernalY = 0; kernalY < _kernalSize; kernalY++) { for (int strideX = 0; strideX < dL_dP.Width; strideX++) { for (int strideY = 0; strideY < dL_dP.Length; strideY++) { // 原逻辑代码 } } } }
4. 考虑硬件加速
如果你的环境支持GPU,GPU原生支持FP16运算,能真正发挥Half的性能优势。可以尝试使用.NET的ML.NET框架或者CUDA绑定来实现GPU加速的卷积运算;如果只能用CPU,当前硬件限制下,float可能仍是更高效的选择。
内容的提问来源于stack exchange,提问作者Invidia
相关产品推荐
相关产品推荐

