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

ref struct构造器中Switch语句比直接赋值更快?求技术解析

ref struct构造器中两种字段赋值方式的性能差异疑惑

我正在开展ref struct与文本值类型的测试,编写struct构造器时发现一个奇怪的现象:通过Switch语句控制ref变量结合循环的方式,居然比直接手动赋值struct字段的速度更快。我原本以为无需布尔判断和索引递增的直接赋值会被内联执行,性能应该更优,对这个结果充满疑惑,同时也不确定用顶级语句做这类性能测试的有效性。

测试代码

using System.Diagnostics;

const string testText = $"""
    Lorem ipsum dolor sit amet, consectetur adipiscing elit.  
    Quisque orci purus, vulputate vulputate
    """;
const int load_Steps = 250;
CharblockExample bySetBlock;
CharblockExample bySwitchBlock;
var watcher = Stopwatch.StartNew();
var results = new List<(TimeSpan tic1, TimeSpan tic2)>();
try
{
    for (int i = 0; i < load_Steps; i++)
    {
        var start_tics = watcher.Elapsed;
        bySwitchBlock = new(testText.AsSpan(), true);
        var ticBlock2 = watcher.Elapsed - start_tics;

        start_tics = watcher.Elapsed;
        bySetBlock = new(testText.AsSpan(), false);
        var ticBlock1 = watcher.Elapsed - start_tics;
        
        Console.WriteLine($"{nameof(bySetBlock)} and {nameof(bySwitchBlock)}: {ticBlock1} vs {ticBlock2}");
        results.Add((ticBlock1, ticBlock2));
    }
}
finally
{
    watcher.Stop();
    Console.WriteLine();
    var overall_1 = results.Sum(element => element.tic1.Microseconds);
    var overall_2 = results.Sum(selector => selector.tic2.Microseconds);
    var overall_result = overall_1 - overall_2;

    Console.WriteLine($"overall analysis: {(overall_1 is < 0
        ? $"{nameof(bySetBlock)}    was {Math.Abs(overall_result)} ms faster than {nameof(bySwitchBlock)}" 
        : $"{nameof(bySwitchBlock)} was {Math.Abs(overall_result)} ms faster than {nameof(bySetBlock)}"
    )}");
}

public readonly ref struct CharblockExample
{
    private readonly int inputtedLength;
    private readonly bool _input = false;
    private readonly char //char spots
    #region Char-Spots
        _0, _1, _2, _3, _4, _5,
        _6, _7, _8, _9, _10;
    #endregion

    public CharblockExample(ReadOnlySpan<char> chars, bool performTest)
    {
        _input = true;
        Span<char> arr = stackalloc char[MAX_LENGTH];
        inputtedLength = chars.Length;
        if (chars.Length > MAX_LENGTH)
            throw new ArgumentOutOfRangeException(nameof(chars), $"""
                The provided char array is longer than the max of 256 chars for this block.
                """);

        for (int i = 0; i < MAX_LENGTH; i++)
        {
            if (i >= chars.Length) break;
            arr[i] = chars[i];
        }

        if (performTest)
        {
            int pos = 0;
            ref var _ref = ref _1;
            while (pos < MAX_LENGTH - 1)
            {
                switch (pos)
                {
                    case 0: _ref = ref _0; break;
                    case 1: _ref = ref _1; break;
                    case 2: _ref = ref _2; break;
                    case 3: _ref = ref _3; break;
                    case 4: _ref = ref _4; break;
                    case 5: _ref = ref _5; break;
                    case 6: _ref = ref _6; break;
                    case 7: _ref = ref _7; break;
                    case 8: _ref = ref _8; break;
                    case 9: _ref = ref _9; break;
                    case 10: _ref = ref _10; break;
                }
                pos++;
                _ref = arr[pos];
            }
        }
        else
        {
            _0 = arr[0]; _1 = arr[1]; _2 = arr[2]; _3 = arr[3]; _4 = arr[4];
            _5 = arr[5]; _6 = arr[6]; _7 = arr[7]; _8 = arr[8]; _9 = arr[9];
            _10 = arr[10];
        }
    }    
    public char[] CharArr => new[]
    {
        _0,   _1,   _2,   _3,   _4,   _5,
        _6,   _7,   _8,   _9,  _10
    };

    private const int MAX_DISPLAYABLE = 15;
    private const int MAX_LENGTH = 256;
}

测试结果

overall analysis: bySwitchBlock was 1240 ms faster than bySetBlock

性能差异核心解析

1. JIT优化的针对性差异

直接手动赋值的代码看似简洁,但JIT编译器对连续的独立字段赋值不会做特殊合并优化,每条赋值都是单独的内存写入指令。而switch+ref变量的方式,本质是通过指针跳转复用赋值逻辑,JIT会将switch编译为跳转表(而非逐个判断的条件分支),循环内的操作会被优化为紧凑的内存访问序列,减少指令冗余。

2. 内存访问的局部性优势

ref struct的字段在栈上连续布局,但直接赋值时每个字段的地址是硬编码的;通过ref变量动态切换目标地址,JIT会生成更高效的地址计算逻辑,循环优化后内存访问的局部性更好,CPU缓存命中率更高,间接提升了执行速度。

3. 测试代码的隐藏偏差

你的测试存在几个影响结果的变量:

  • 测试顺序问题:每次循环先执行bySwitchBlock构造,JIT会优先优化先执行的代码路径,导致bySetBlock未获得同等优化;
  • 循环次数不足:250次循环的量级太小,容易被GC抖动、CPU调度等外部因素干扰,无法反映真实性能差异;
  • 非测试逻辑的干扰:stackalloc char[MAX_LENGTH]在两个分支都执行,JIT对栈分配的处理差异可能影响测试结果,应将这部分逻辑抽离,确保仅对比赋值逻辑。

4. ref变量的底层特性

.NET中ref变量对应CPU寄存器级的指针操作,switch跳转表的分支切换成本极低,几乎可以忽略。相比之下,连续字段赋值需要多次独立的内存写入指令,总指令数更多,执行时间自然更长。

优化建议

  • 调整测试顺序:循环中交替执行两种构造逻辑,或分开执行两个测试分支,避免JIT优化的顺序偏差;
  • 提升测试量级:将循环次数提升至10万次以上,减少随机因素干扰;
  • 使用专业测试框架:用BenchmarkDotNet替代手动Stopwatch,它会自动处理JIT预热、GC控制、统计分析等问题,结果更可靠;
  • 重构赋值逻辑:若需极致性能,可使用Unsafe类直接操作内存块,一次性将Span复制到struct的内存区域,效率远高于现有两种方式。

内容的提问来源于stack exchange,提问作者James Jonatah

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 16:55:17