听起来您想要一种方法来评估代码的 FPU 绑定程度,或者您使用 FPU 的效率,而不是根据“翻牌”的相同过时定义来计算翻牌数。换句话说,如果每个浮点单元在每个周期都以满负荷运行,您需要一个达到相同峰值的指标。让我们看一下英特尔 Sandy Bridge,看看它会如何摆脱困境。
硬件支持的浮点运算
该芯片支持AVX指令,因此寄存器长 32 个字节(保存 4 个双精度)。超标量架构允许指令重叠,大多数算术指令需要几个周期才能完成,即使新指令可能能够在下一个周期开始。这些语义通常缩写为写延迟/反向吞吐量,值 5/2 意味着指令需要 5 个周期才能完成,但您可以每隔一个周期启动一条新指令(假设操作数可用,因此没有数据依赖而不是等待内存)。
每个内核有三个浮点运算单元,但第三个与我们的讨论无关,我们将相关的两个称为 A 和 M 单元,因为它们的主要功能是加法和乘法。示例说明(参见Agner Fog 的表格)
vaddpd
: 打包加法,占用单元A 1个周期,延迟/逆吞吐量为3/1
vmulpd
: 压缩乘法,单位 M,5/1
vmaxpd
:打包选择成对最大值,单位 A,3/1
vdivpd
:压缩除法,单元 M(和一些 A),21/20 到 45/44,取决于输入
vsqrtpd
:压缩平方根,一些 A 和 M,21/21 到 43/43,取决于输入
vrsqrtps
: 用于单精度输入的压缩低精度倒数平方根 (8 floats
)
可以与哪些内容重叠vdivpd
并且vsqrtpd
显然是微妙的和 AFAIK 的精确语义,在任何地方都没有记录。在大多数用途中,我认为重叠的可能性很小,尽管手册中的措辞表明多线程可能会在本指令中提供更多重叠的可能性。如果我们在每个周期都开始一个vaddpd
和,我们可以达到峰值翻牌vmulpd
,每个周期总共有 8 次翻牌。密集矩阵-矩阵乘法 ( dgemm
) 可以合理地接近该峰值。
在计算特殊指令的触发器时,我会查看占用了多少 FPU。假设在您的输入范围内,vdivpd
平均需要 24 个周期才能完成,完全占用单元 M,但加法可以(如果可用)同时执行一半周期。FPU 能够在这些循环中执行 24 次压缩乘法和 24 次压缩加法(完全交错vaddpd
和vmulpd
),但是对于vdivpd
,我们能做的最好的事情是 12 次额外的压缩加法。如果我们假设进行除法的最佳方法是使用硬件(合理),我们可能会将其计vdivpd
为 36 个压缩“触发器”,这表明我们应该将每个标量除法计为 36 个“触发器”。
使用倒数平方根,有时可以击败硬件,尤其是在不需要完全准确度或输入范围很窄的情况下。如上所述,该vrsqrtps
指令非常便宜,因此(如果是单精度)您可以先执行一个vrsqrtps
,然后执行一到两次牛顿迭代来清理。这些牛顿迭代只是
y *= (3 - x*y*y)*0.5;
如果需要执行许多这些操作,这可能比简单的评估要快得多y = 1/sqrt(x)
。在硬件近似倒数平方根可用之前,一些性能敏感的代码使用臭名昭著的整数运算来找到牛顿迭代的初始猜测。
库提供的数学函数
我们可以将类似的启发式应用于库提供的数学函数。您可以分析以确定 SSE 指令的数量,但正如我们所讨论的,这不是全部,一个花费所有时间评估特殊功能的程序可能看起来不会接近峰值,这可能是真的,但不是告诉你所有的时间都花在了你无法控制的 FPU 上,这没什么用。
我建议使用一个好的向量数学库作为基线(例如英特尔的 VML,MKL 的一部分)。测量每个调用的周期数,并乘以在该周期数内可实现的峰值触发器。因此,如果一个压缩指数需要 50 个周期来评估,则将其计算为 100 次浮点数乘以寄存器宽度。不幸的是,向量数学库有时很难调用并且没有所有的特殊函数,所以你最终可能会做标量数学,在这种情况下,你会将我们假设的标量指数计算为 100 次失败(尽管它可能仍然需要 50周期,所以如果所有时间都花在评估这些指数上,你只会得到 25% 的“峰值”)。
正如其他人所提到的,您可以使用 PAPI 或各种接口计算周期和硬件事件计数器。rdtsc
对于简单的循环计数,您可以使用带有内联汇编代码段的指令直接读取循环计数器。