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
相关产品推荐
相关产品推荐

