You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用.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类型的二维数组。


问题原因分析

  1. CPU对Half的硬件支持不足:x86/64架构的CPU没有原生的16位浮点运算单元,所有Half类型的运算都需要先转换为32位float计算,再转回Half。在你的四重循环这种高频运算场景下,频繁的类型转换会带来巨大的额外开销,直接拖慢速度。
  2. readonly结构体的频繁实例化:你的Color是readonly struct,每次执行加减乘除运算都会创建新的Color实例。循环中反复创建值类型实例(即使在栈上)也会增加运算指令数,放大性能损耗。
  3. 循环顺序导致缓存命中率低:当前循环顺序是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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 17:25:05