当您在 '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 的自由度;由于 max1
和 max2
是独立的,因此可以并行计算。
所以基本上修改后的循环可以 运行 与第一个循环一样快 每次迭代:
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 执行并不完美)。
我做了一些代码性能测试,我想知道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 的自由度;由于 max1
和 max2
是独立的,因此可以并行计算。
所以基本上修改后的循环可以 运行 与第一个循环一样快 每次迭代:
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 执行并不完美)。