当您在 'for' 循环中访问不同的值时,CPU 缓存如何工作?

How CPU caching works when you get access to different value in a 'for' loop?

我做了一些代码性能测试,我想知道CPU缓存在这种情况下是如何工作的:

这是一个经典的循环示例:

        private static readonly short[] _values;

        static MyClass()
        {
            var random = new Random();
            _values = Enumerable.Range(0, 100)
                                .Select(x => (short)random.Next(5000))
                                .ToArray();
        }

        public static void Run()
        {
            short max = 0;
            for (var index = 0; index < _values.Length; index++)
            {
                max = Math.Max(max, _values[index]);
            }
        }

这里是获得同样东西但性能更高的具体情况:

        private static readonly short[] _values;

        static MyClass()
        {
            var random = new Random();
            _values = Enumerable.Range(0, 100)
                                .Select(x => (short)random.Next(5000))
                                .ToArray();
        }

        public static void Run()
        {
            short max1 = 0;
            short max2 = 0;
            for (var index = 0; index < _values.Length; index+=2)
            {
                max1 = Math.Max(max1, _values[index]);
                max2 = Math.Max(max2, _values[index + 1]);
            }
            short max = Math.Max(max1, max2);
        }

所以我很想知道为什么第二个比第一个更有效率。 我知道这是一个关于 CPU 缓存的故事,但我不明白它是如何发生的(比如值在循环之间没有被读取两次)。

编辑:

.NET 核心 4.6.27617.04 2.1.11 英特尔酷睿 i7-7850HQ 2.90GHz 64 位

调用 5000 万次:

MyClass1: => 00:00:06.0702028

MyClass2: => 00:00:03.8563776 (-36 %)

最后一个指标是循环展开的指标。

缓存

缓存在cpu中工作,例如pre-loads内存中的下几行代码并将其存储在CPU缓存中,这可能是数据、指针、变量价值观等等等等

代码块

在你的两个代码块之间,差异可能不会出现在语法中,请尝试将你的代码转换为 IL(由 JIT(just-in-time 编译器)执行的 c# 的中间运行时语言)参见 ref工具和资源。

或者只是反编译您的 built/compiled 代码并检查编译器 "optimized it" 在使用下面的反编译器制作 dll/exe 文件时如何。

其他性能优化

参考文献:

这种情况下的性能差异与缓存无关 - 您只有 100 个值 - 它们在您生成它们时已经完全适合 L2 缓存。

差异是由于 out-of-order execution

现代 CPU 具有多个执行单元,即使在 single-threaded 应用程序中也可以同时执行多个操作。

但是你的循环对于现代 CPU 来说是有问题的,因为它有一个依赖项:

        short max = 0;
        for (var index = 0; index < _values.Length; index++)
        {
            max = Math.Max(max, _values[index]);
        }

此处每个后续迭代都依赖于前一个迭代的值 max,因此 CPU 被迫按顺序计算它们。

您修改后的循环增加了 CPU 的自由度;由于 max1max2 是独立的,因此可以并行计算。

所以基本上修改后的循环可以 运行 与第一个循环一样快 每次迭代

        short max1 = 0;
        short max2 = 0;
        for (var index = 0; index < _values.Length; index+=2)
        {
            max1 = Math.Max(max1, _values[index]);
            max2 = Math.Max(max2, _values[index + 1]);
        }

但是它有一半的迭代,所以最后你得到了显着的加速(不是 2 倍,因为 out-of-order 执行并不完美)。