Roofline 模型:计算运营强度
Roofline model: calculating operational intensity
假设我有一个这样的玩具循环
float x[N];
float y[N];
for (int i = 1; i < N-1; i++)
y[i] = a*(x[i-1] - x[i] + x[i+1])
我假设我的缓存行是 64 Byte(即足够大)。然后我将(每帧)基本上有 2 次访问 RAM 和 3 次 FLOP:
- 1(缓存)读访问:加载所有 3
x[i-1], x[i], x[i+1]
- 1 次写访问:存储
y[i]
- 3 FLOP (1 mul, 1 add, 1 sub)
运算强度为ergo
OI = 3 FLOP/(2 * 4 BYTE)
现在如果我这样做会发生什么
float x[N];
for (int i = 1; i < N-1; i++)
x[i] = a*(x[i-1] - x[i] + x[i+1])
请注意,不再有 y
。这是否意味着现在我有一个单一的 RAM 访问
- 1(缓存)read/write:加载
x[i-1], x[i], x[i+1]
,存储x[i]
或者还有 2 次 RAM 访问
- 1(缓存)读取:加载
x[i-1], x[i], x[i+1]
- 1(缓存)写:存储
x[i]
因为操作强度OI在任何一种情况下都会不同。任何人都可以告诉这件事吗?或者澄清一些事情。谢谢
免责声明:直到今天我才听说过 roofline 性能模型。据我所知,它试图计算算法 "arithmetic intensity" 的理论界限,即访问的每字节数据的 FLOPS 数。随着 N
的大小变大,这种度量可能有助于比较类似算法,但对于预测真实世界的性能不是很有帮助。
作为一般经验法则,现代处理器执行指令的速度比 fetch/store 数据快得多(随着数据开始增长到大于缓存的大小,这一点变得更加明显)。因此,与人们可能期望的相反,具有较高算术强度的循环可能 运行 比具有较低算术强度的循环快得多;随着 N
规模的扩大,最重要的是接触的数据总量(只要内存仍然比处理器慢得多,这一点就会成立,就像当今常见的台式机和服务器系统一样)。
简而言之,不幸的是,x86 CPUs 太复杂了,无法用如此简单的模型准确描述。在到达 RAM 之前,对内存的访问会经过几层缓存(通常是 L1、L2 和 L3)。也许你所有的数据都适合 L1——你第二次 运行 你的循环可能根本没有 RAM 访问。
而且不仅仅是数据缓存。不要忘记代码也在内存中并且必须加载到指令缓存中。每个 read/write 也完成 from/to 一个虚拟地址,该地址由硬件 TLB 支持(在极端情况下会触发页面错误,例如导致 OS 写入页面到循环中间的磁盘)。所有这一切都假设您的程序正在占用所有硬件(在非实时 OSes 中根本不是这种情况,因为其他进程和线程正在竞争相同的有限资源)。
最后,执行本身并不是(直接)完成内存读写,而是先将数据加载到寄存器中(然后存储结果)。
编译器如何分配寄存器、是否尝试循环展开、自动矢量化、指令调度模型(交错指令以避免指令之间的数据依赖)等也会影响算法的实际吞吐量。
因此,最后,根据生成的代码、CPU 模型、处理的数据量以及各种缓存的状态,算法的延迟将按数量级变化。因此,不能仅通过检查代码(甚至生成的程序集)来确定循环的操作强度,因为还有许多其他(非线性)因素在起作用。
不过,为了解决您的实际问题,据我在 definition outlined here 中所见,第二个循环平均算作每次迭代的一个额外的 4 字节访问,因此它的 OI 将是θ(3N FLOPS / 4N 字节)。直观上,这是有道理的,因为缓存已经加载了数据,写入可以直接更改缓存而不是返回主内存(数据最终确实必须写回,但是,这个要求与第一次没有变化循环)。
假设我有一个这样的玩具循环
float x[N];
float y[N];
for (int i = 1; i < N-1; i++)
y[i] = a*(x[i-1] - x[i] + x[i+1])
我假设我的缓存行是 64 Byte(即足够大)。然后我将(每帧)基本上有 2 次访问 RAM 和 3 次 FLOP:
- 1(缓存)读访问:加载所有 3
x[i-1], x[i], x[i+1]
- 1 次写访问:存储
y[i]
- 3 FLOP (1 mul, 1 add, 1 sub)
运算强度为ergo
OI = 3 FLOP/(2 * 4 BYTE)
现在如果我这样做会发生什么
float x[N];
for (int i = 1; i < N-1; i++)
x[i] = a*(x[i-1] - x[i] + x[i+1])
请注意,不再有 y
。这是否意味着现在我有一个单一的 RAM 访问
- 1(缓存)read/write:加载
x[i-1], x[i], x[i+1]
,存储x[i]
或者还有 2 次 RAM 访问
- 1(缓存)读取:加载
x[i-1], x[i], x[i+1]
- 1(缓存)写:存储
x[i]
因为操作强度OI在任何一种情况下都会不同。任何人都可以告诉这件事吗?或者澄清一些事情。谢谢
免责声明:直到今天我才听说过 roofline 性能模型。据我所知,它试图计算算法 "arithmetic intensity" 的理论界限,即访问的每字节数据的 FLOPS 数。随着 N
的大小变大,这种度量可能有助于比较类似算法,但对于预测真实世界的性能不是很有帮助。
作为一般经验法则,现代处理器执行指令的速度比 fetch/store 数据快得多(随着数据开始增长到大于缓存的大小,这一点变得更加明显)。因此,与人们可能期望的相反,具有较高算术强度的循环可能 运行 比具有较低算术强度的循环快得多;随着 N
规模的扩大,最重要的是接触的数据总量(只要内存仍然比处理器慢得多,这一点就会成立,就像当今常见的台式机和服务器系统一样)。
简而言之,不幸的是,x86 CPUs 太复杂了,无法用如此简单的模型准确描述。在到达 RAM 之前,对内存的访问会经过几层缓存(通常是 L1、L2 和 L3)。也许你所有的数据都适合 L1——你第二次 运行 你的循环可能根本没有 RAM 访问。
而且不仅仅是数据缓存。不要忘记代码也在内存中并且必须加载到指令缓存中。每个 read/write 也完成 from/to 一个虚拟地址,该地址由硬件 TLB 支持(在极端情况下会触发页面错误,例如导致 OS 写入页面到循环中间的磁盘)。所有这一切都假设您的程序正在占用所有硬件(在非实时 OSes 中根本不是这种情况,因为其他进程和线程正在竞争相同的有限资源)。
最后,执行本身并不是(直接)完成内存读写,而是先将数据加载到寄存器中(然后存储结果)。
编译器如何分配寄存器、是否尝试循环展开、自动矢量化、指令调度模型(交错指令以避免指令之间的数据依赖)等也会影响算法的实际吞吐量。
因此,最后,根据生成的代码、CPU 模型、处理的数据量以及各种缓存的状态,算法的延迟将按数量级变化。因此,不能仅通过检查代码(甚至生成的程序集)来确定循环的操作强度,因为还有许多其他(非线性)因素在起作用。
不过,为了解决您的实际问题,据我在 definition outlined here 中所见,第二个循环平均算作每次迭代的一个额外的 4 字节访问,因此它的 OI 将是θ(3N FLOPS / 4N 字节)。直观上,这是有道理的,因为缓存已经加载了数据,写入可以直接更改缓存而不是返回主内存(数据最终确实必须写回,但是,这个要求与第一次没有变化循环)。