小编典典

为英特尔 Sandybridge 系列 CPU 中的管道取消优化程序

all

为了完成这项任务,我已经绞尽脑汁一个星期了,我希望这里有人能带领我走向正确的道路。让我从导师的指示开始:

你的任务与我们的第一个实验任务相反,它是优化素数程序。你在这个作业中的目的是使程序悲观,即让它运行得更慢。这两个都是 CPU
密集型程序。它们需要几秒钟才能在我们的实验室 PC​​ 上运行。你不能改变算法。

要取消优化程序,请使用您对英特尔 i7 管道如何运行的了解。想象一下重新排序指令路径以引入 WAR、RAW
和其他危险的方法。想办法最小化缓存的有效性。无能为力。

该作业提供了 Whetstone 或 Monte-Carlo 程序的选择。缓存有效性注释大多只适用于 Whetstone,但我选择了 Monte-Carlo
模拟程序:

// Un-modified baseline for pessimization, as given in the assignment
#include <algorithm>    // Needed for the "max" function
#include <cmath>
#include <iostream>

// A simple implementation of the Box-Muller algorithm, used to generate
// gaussian random numbers - necessary for the Monte Carlo method below
// Note that C++11 actually provides std::normal_distribution<> in 
// the <random> library, which can be used instead of this function
double gaussian_box_muller() {
  double x = 0.0;
  double y = 0.0;
  double euclid_sq = 0.0;

  // Continue generating two uniform random variables
  // until the square of their "euclidean distance" 
  // is less than unity
  do {
    x = 2.0 * rand() / static_cast<double>(RAND_MAX)-1;
    y = 2.0 * rand() / static_cast<double>(RAND_MAX)-1;
    euclid_sq = x*x + y*y;
  } while (euclid_sq >= 1.0);

  return x*sqrt(-2*log(euclid_sq)/euclid_sq);
}

// Pricing a European vanilla call option with a Monte Carlo method
double monte_carlo_call_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
  double S_adjust = S * exp(T*(r-0.5*v*v));
  double S_cur = 0.0;
  double payoff_sum = 0.0;

  for (int i=0; i<num_sims; i++) {
    double gauss_bm = gaussian_box_muller();
    S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
    payoff_sum += std::max(S_cur - K, 0.0);
  }

  return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}

// Pricing a European vanilla put option with a Monte Carlo method
double monte_carlo_put_price(const int& num_sims, const double& S, const double& K, const double& r, const double& v, const double& T) {
  double S_adjust = S * exp(T*(r-0.5*v*v));
  double S_cur = 0.0;
  double payoff_sum = 0.0;

  for (int i=0; i<num_sims; i++) {
    double gauss_bm = gaussian_box_muller();
    S_cur = S_adjust * exp(sqrt(v*v*T)*gauss_bm);
    payoff_sum += std::max(K - S_cur, 0.0);
  }

  return (payoff_sum / static_cast<double>(num_sims)) * exp(-r*T);
}

int main(int argc, char **argv) {
  // First we create the parameter list                                                                               
  int num_sims = 10000000;   // Number of simulated asset paths                                                       
  double S = 100.0;  // Option price                                                                                  
  double K = 100.0;  // Strike price                                                                                  
  double r = 0.05;   // Risk-free rate (5%)                                                                           
  double v = 0.2;    // Volatility of the underlying (20%)                                                            
  double T = 1.0;    // One year until expiry

  // Then we calculate the call/put values via Monte Carlo                                                                          
  double call = monte_carlo_call_price(num_sims, S, K, r, v, T);
  double put = monte_carlo_put_price(num_sims, S, K, r, v, T);

  // Finally we output the parameters and prices                                                                      
  std::cout << "Number of Paths: " << num_sims << std::endl;
  std::cout << "Underlying:      " << S << std::endl;
  std::cout << "Strike:          " << K << std::endl;
  std::cout << "Risk-Free Rate:  " << r << std::endl;
  std::cout << "Volatility:      " << v << std::endl;
  std::cout << "Maturity:        " << T << std::endl;

  std::cout << "Call Price:      " << call << std::endl;
  std::cout << "Put Price:       " << put << std::endl;

  return 0;
}

我所做的更改似乎将代码运行时间增加了一秒钟,但我不完全确定在不添加代码的情况下我可以更改什么来停止管道。指向正确的方向会很棒,我很感激任何回应。


更新:给这个作业的教授发布了一些细节

亮点是:

  • 这是一所社区大学的第二学期建筑课(使用 Hennessy 和 Patterson 教科书)。
  • 实验室计算机有 Haswell CPU
  • 学生已经接触到CPUID指令以及如何确定缓存大小,以及内在函数和CLFLUSH指令。
  • 允许任何编译器选项,内联 asm 也是如此。
  • 编写自己的平方根算法被宣布为超出范围

Cowmoogun对元线程的评论表明,编译器优化是否可能是其中的一部分并不清楚,并且假设-O0)运行时间增加 17% 是合理的。

所以听起来这项作业的目标是让学生重新排序现有的工作,以减少指令级并行性或类似的事情,但人们深入研究并学到更多东西并不是一件坏事。


请记住,这是一个计算机体系结构问题,而不是关于如何使 C++ 变慢的问题。


阅读 86

收藏
2022-04-06

共1个答案

小编典典

重要的背景阅读: Agner Fog 的 microarch pdf ,可能还有
Ulrich Drepper 的What Every Programmer Should Know About
Memory
。另请参阅x86标签 wiki 中的其他链接,尤其是 Intel 的优化手册,以及 David Kanter对 Haswell
微架构的分析,以及图表

非常酷的任务;比我见过的要求学生优化一些代码gcc-O0的那些要好得多,学习了一堆在实际代码中无关紧要的技巧。在这种情况下,您被要求了解
CPU 管道并使用它来指导您的反优化工作,而不仅仅是盲目猜测。 这个最有趣的部分是用“恶魔般的无能”而不是故意的恶意来证明每一次悲观。


作业用语和代码的问题

此代码的特定于 uarch
的选项是有限的。它不使用任何数组,大部分成本是调用exp/log库函数。没有明显的方法来获得或多或少的指令级并行性,并且循环携带的依赖链非常短。

仅仅通过重新排列表达式来改变依赖关系,以减少ILP的危害,很难减慢速度。

英特尔 Sandybridge 系列 CPU 是激进的无序设计,它花费大量晶体管和功率来寻找并行性并避免会影响经典 RISC
有序流水线的
危险(依赖性)
。通常,唯一减慢它的传统危害是导致吞吐量受到延迟限制的原始“真正”依赖关系。

由于寄存器重命名,寄存器的WAR 和 WAW危害
几乎不是问题
。(除了popcnt//lzcnt,它们的目的地对 IntelCPUtzcnt有,即使它应该是只写的)。

对于内存排序,现代 CPU 使用存储缓冲区来延迟提交到缓存直到退休,同时也避免了 WAR 和 WAW
危害
。另请参阅有关什么是存储缓冲区的答案,并且对于OoO exec 将执行与其他内核可以看到的内容分离必不可少。

为什么 mulss 在 Haswell 上只需要 3 个周期,与 Agner 的指令表不同?(展开具有多个累加器的 FP循环)有更多关于寄存器重命名和隐藏 FP 点积循环中的 FMA 延迟的信息。


“i7”品牌名称是随着 Nehalem(Core2 的继任者)引入的 ,一些英特尔手册甚至说 Core i7,而它们似乎意味着
Nehalem,但他们
Sandybridge
和后来的微架构保留了“i7”品牌。 SnB
是 P6 家族进化成一个新物种 SnB 家族的时候
。在许多方面,Nehalem 与 Pentium III 相比与 Sandybridge 有更多共同点(例如,寄存器读取停顿(即 ROB
读取停顿)不会在 SnB 上发生,因为它更改为使用物理寄存器文件。还有一个 uop 缓存和不同的内部uop 格式)。 “i7架构”这个词没用
,因为将 SnB 家族与 Nehalem 而不是 Core2 分组几乎没有意义。(不过,Nehalem 确实引入了共享的包容性 L3
缓存架构,用于将多个内核连接在一起。而且还集成了 GPU。所以芯片级的命名更有意义。)


恶魔无能可以证明的好主意的总结

即使是极其无能的人也不太可能添加明显无用的工作或无限循环,而将 C++/Boost 类弄得一团糟超出了作业的范围。

  • 具有单个 共享 std::atomic<uint64_t>循环计数器的多线程,因此发生正确的迭代总数。原子 uint64_t 与-m32 -march=i586. 对于加分,安排它错位,并以不均匀的分割(不是 4:4)跨越页面边界。
  • 其他一些非原子变量的错误共享 -> 内存顺序错误推测管道清除,以及额外的缓存未命中。
  • 不是-在 FP 变量上使用,而是将高字节与 0x80 进行异或以翻转符号位,从而导致 存储转发停止
  • 独立计时每次迭代,甚至比RDTSC. 例如CPUID/RDTSC或进行系统调用的时间函数。序列化指令本质上是对管道不友好的。
  • 将乘以常数更改为除以它们的倒数(“为了便于阅读”)。 div 很慢并且没有完全流水线化。
  • 使用 AVX (SIMD) 向量化乘法/平方,但vzeroupper在调用标量数学库exp()log()函数之前无法使用,导致 AVX <->SSE 转换停止
  • 将 RNG 输出存储在链表中,或者存储在乱序遍历的数组中。每次迭代的结果相同,最后求和。

此答案中也包含但未从摘要中排除:建议在非流水线 CPU 上同样慢,或者即使在恶魔无能的情况下似乎也不合理。例如,许多 gimp-the-compiler
想法会产生明显不同/更糟糕的 asm。


多线程严重

也许将 OpenMP
用于迭代次数很少的多线程循环,其开销远大于速度增益。但是,您的蒙特卡洛代码具有足够的并行性来实际获得加速,尤其是。如果我们成功地使每次迭代变慢。(每个线程计算一个
partial payoff_sum,在最后添加)。 #omp parallel在那个循环上可能是一种优化,而不是悲观。

多线程,但强制两个线程共享相同的循环计数器(atomic增量,因此迭代的总数是正确的)。这似乎是荒谬的逻辑。这意味着使用static变量作为循环计数器。这证明了使用atomicfor
循环计数器的合理性,并创建了实际的缓存行乒乓(只要线程不在具有超线程的同一物理内核上运行;这可能不会
那么 慢)。无论如何,这 lock inc. 并且在 32 位系统上lock cmpxchg8b以原子方式增加竞争uint64_t将不得不在循环中重试,而不是让硬件仲裁 atomic inc

还要创建 假共享 ,其中多个线程将其私有数据(例如 RNG 状态)保存在同一缓存行的不同字节中。
(关于它的英特尔教程,包括要查看的性能计数器)这有一个特定于微体系结构的方面 :英特尔 CPU 推测
不会 发生内存错误排序,并且有一个内存顺序机器清除性能事件来检测这一点,至少在 P4
。对哈斯韦尔的处罚可能没有那么大。正如该链接指出的那样,locked
指令假设这会发生,避免错误推测。正常负载推测其他内核不会在负载执行和程序顺序退出之间使高速缓存行无效(除非你使用pause)。没有
ed
指令的真正共享lock通常是一个错误。将非原子共享循环计数器与原子情况进行比较会很有趣。真正悲观的是,保留共享原子循环计数器,并导致其他变量在相同或不同缓存行中的错误共享。


随机 uarch 特定的想法:

如果您可以引入 任何不可预测的分支 ,那将使代码大大悲观。现代 x86 CPU 具有相当长的管道,因此错误预测会花费大约 15 个周期(从 uop
缓存运行时)。


依赖链:

我认为这是任务的预期部分之一。

通过选择具有一个长依赖链而不是多个短依赖链的操作顺序来击败 CPU 利用指令级并行性的能力。除非您使用 ,否则编译器不允许更改 FP
计算的操作顺序-ffast-math,因为这会更改结果(如下所述)。

要真正做到这一点,请增加循环携带的依赖链的长度。然而,没有什么比这更明显了:所写的循环具有非常短的循环携带依赖链:只是一个 FP 添加。(3
个周期)。多次迭代可以同时进行计算,因为它们可以payoff_sum +=在上一次迭代结束之前开始。(log()exp采取许多指令,但不比Haswell 的用于查找并行性的乱序窗口多得多:ROB 大小=192
融合域微指令,调度器大小=60 未融合域微指令
.
一旦当前迭代的执行进展到足以为下一次迭代发出的指令腾出空间,当旧指令离开执行单元时,它的任何输入就绪的部分(即独立/单独的 dep
链)都可以开始执行免费(例如,因为它们的瓶颈是延迟,而不是吞吐量。)。

RNG 状态几乎肯定会是一个比addps.


使用更慢/更多的 FP 操作(尤其是更多的除法):

除以 2.0 而不是乘以 0.5,以此类推。FP 乘法在 Intel 设计中大量流水线化,在 Haswell 及更高版本上每 0.5c 吞吐量有一个。
FPdivsd/divpd只是部分流水线。(尽管 Skylake 的每 4c 吞吐量令人印象深刻divpd xmm,延迟为
13-14c,而 Nehalem (7-22c) 上根本没有流水线)。

do { ...; euclid_sq = x*x + y*y; } while (euclid_sq >= 1.0);显然是在测试一段距离,所以显然它适合sqrt()它。:P (sqrt甚至比div) 还要慢。

正如@Paul Clayton 建议的那样,用关联/分配等价物重写表达式可以引入更多工作(只要您不使用-ffast-math允许编译器重新优化)。
(exp(T*(r-0.5*v*v))可以变成exp(T*r - T*v*v/2.0). 请注意,虽然实数数学是关联的,但浮点数学
不是
,即使不考虑溢出/NaN(这就是-ffast- math默认情况下不启用的原因)。

如果您可以将计算缩小到非常小的数字,那么 当对两个正常数字的操作产生非正规数时,FP 数学运算需要大约 120 个额外的周期来捕获微码
。有关确切数字和详细信息,请参阅 Agner Fog 的 microarch pdf。这不太可能,因为您有很多乘法,因此比例因子将被平方并一直下溢到
0.0。我看不出有任何方法可以证明无能(甚至是恶魔般的)必要的缩放是合理的,只有故意的恶意。


如果你可以使用内在函数(<immintrin.h>

用于movnti从缓存中驱逐您的数据。恶魔:它是新的和弱排序的,所以应该让 CPU
运行得更快,对吧?或者在某人有这样做的危险的情况下查看链接的问题(对于只有一些位置很热的分散写入)。 clflush可能没有恶意是不可能的。

在 FP 数学运算之间使用整数洗牌来导致绕过延迟。

在没有正确使用的情况下混合 SSE 和 AVX 指令vzeroupper会导致前 Skylake
中的大停顿
(以及Skylake
的不同惩罚)。即使没有这一点,向量化也可能比标量更糟糕(将数据混入/出向量所花费的周期比一次执行
4 次 Monte-Carlo 迭代的 add/sub/mul/div/sqrt 操作所节省的周期要多,使用 256b 个向量) . add/sub/mul
执行单元是完全流水线和全宽的,但是 256b 向量上的 div 和 sqrt 不如 128b 向量(或标量)上的快,因此对于double.

exp()并且log()没有硬件支持,因此该部分需要将向量元素提取回标量并单独调用库函数,然后将结果重新排列回向量中。libm 通常编译为仅使用
SSE2,因此将使用标量数学指令的传统 SSE 编码。如果您的代码使用 256b
向量并exp在没有执行vzeroupper第一次调用的情况下进行调用,那么您就会停滞不前。vmovsd返回后,像将下一个向量元素设置为 arg
for的 AVX-128 指令exp也将停止。然后exp()在运行 SSE 指令时再次停止。

有关此代码,另请参阅Nathan Kurz 对英特尔数学库与 glibc
的实验
。未来的 glibc
将带有等的矢量化实现exp()


如果针对前 IvB,或特别是。Nehalem,尝试让 gcc 导致 16 位或 8 位操作后跟 32 位或 64
位操作的部分寄存器停止。在大多数情况下,gcc 会movzx在 8 位或 16 位操作之后使用,但这里是 gcc
修改ah然后读取的情况ax


使用(内联)asm:

使用(内联)asm,您可以破坏 uop 缓存:不适合三个 6uop 缓存行的 32B 代码块会强制从 uop
缓存切换到解码器。在内部循环内的分支目标上ALIGN使用许多单字节nop而不是几个 long的无能(如 NASM
的默认值)可能会解决问题。nop或者将对齐填充放在标签之后,而不是之前。:P 这仅在前端是瓶颈时才重要,如果我们成功地对其余代码进行悲观化,这将不重要。

使用自修改代码来触发管道清除(又名机器核弹)。

16 位指令的LCP 停顿,立即数太大而无法放入 8 位,不太可能有用。SnB 及更高版本上的 uop
缓存意味着您只需支付一次解码罚款。在 Nehalem(第一个 i7)上,它可能适用于不适合 28 uop 循环缓冲区的循环。gcc
有时会生成这样的指令,即使-mtune=intel它可以使用 32 位指令也是如此。


计时的一个常见习语是CPUID(to serialize)thenRDTSC。每次迭代都用CPUID/分开计时RDTSC,以确保RDTSC不会用之前的指令重新排序,这会大大减慢 速度
。(在现实生活中,计时的聪明方法是将所有迭代计时在一起,而不是分别计时并将它们相加)。


导致大量缓存未命中和其他内存减慢

将 aunion { double d; char a[8]; }用于您的一些变量。 通过仅对其中一个字节执行窄存储(或读取-修改-
写入)导致存储转发停止

。(该 wiki 文章还涵盖了许多其他用于加载/存储队列的微架构内容)。例如, 使用 XOR 0x80 仅在高字节上
翻转符号double,而不是-运算符。极其无能的开发人员可能听说过 FP
比整数慢,因此尝试尽可能多地使用整数操作。(理论上,编译器仍然可以将其编译为xorps带有类似 的常量-,但对于
x87,编译器必须意识到它正在否定该值,fchs或者将下一个加法替换为减法。)


volatile如果您正在使用而不是使用 ,
进行编译,请-O3使用std::atomic以强制编译器实际存储/重新加载所有位置。全局变量(而不是局部变量)也会强制一些存储/重新加载,但C++
内存模型的弱排序
并不要求编译器一直溢出/重新加载到内存。

用大结构的成员替换本地变量,这样您就可以控制内存布局。

在结构中使用数组进行填充(并存储随机数,以证明它们的存在)。

选择您的内存布局,以便所有内容都进入 L1缓存中同一“集合”中的不同行]。它只有 8 路关联,即每组有 8 个“路”。高速缓存行为 64B。

更好的是, 将事物完全分开 4096B,因为加载对存储到不同页面但在一个页面内具有相同偏移量具有错误的依赖关系 。激进的乱序 CPU
使用Memory Disambiguation
来确定何时可以在不改变结果的情况下重新排序加载和存储
,并且英特尔的实现有误报,可以防止加载过早开始。可能他们只检查页面偏移量以下的位,因此它可以在
TLB 将高位从虚拟页面转换为物理页面之前启动。

用于__attribute__((packed))让您错误对齐变量,以便它们跨越缓存行甚至页面边界。(所以一个负载double需要来自两个缓存行的数据)。未对齐的负载在任何
Intel i7 uarch 中都不会受到惩罚,除非在跨越高速缓存行和页面行时。
缓存行拆分仍然需要额外的周期。Skylake
将页面拆分加载的惩罚从 100 个周期显着降低到 5 个周期。(第 2.1.3
节)
。(并且可以并行进行两次页面遍历)。

an 上的页面拆分atomic<uint64_t>应该是最坏的情况,尤其是。如果它是一页中的 5 个字节和另一页中的 3 个字节,或者不是
4:4。在某些 uarches,IIRC 上,即使是从中间拆分对于使用 16B 向量的高速缓存行拆分也更有效。将所有内容放入一个alignas(4096) struct __attribute((packed))(当然是为了节省空间),包括一个用于存储 RNG
结果的数组。通过在计数器之前使用uint8_tor来实现错位。uint16_t

如果您可以让编译器使用索引寻址模式,那将击败 uop micro-fusion。也许通过使用#defines将简单的标量变量替换为my_data[constant].

如果您可以引入额外的间接级别,那么加载/存储地址不会及早知道,这可能会进一步悲观。


以非连续顺序遍历数组

我认为我们可以首先提出引入数组的不充分理由:它让我们将随机数生成与随机数使用分开。每次迭代的结果也可以存储在一个数组中,以供以后求和(具有更多的恶魔无能)。

对于“最大随机性”,我们可以让一个线程在随机数组上循环,将新的随机数写入其中。使用随机数的线程可以生成随机索引以从中加载随机数。(这里有一些临时工作,但从微架构上讲,它有助于尽早知道加载地址,因此可以在需要加载数据之前解决任何可能的加载延迟。)在不同的内核上拥有读取器和写入器会导致内存排序错误-
推测管道清除(如前文讨论的虚假共享案例)。

为了最大程度地悲观化,以 4096 字节的步幅(即 512 个双精度数)循环遍历您的数组。例如

for (int i=0 ; i<512; i++)
    for (int j=i ; j<UPPER_BOUND ; j+=512)
        monte_carlo_step(rng_array[j]);

所以访问模式是 0, 4096, 8192, …,
8, 4104, 8200, …
16, 4112, 8208, …

这就是您double rng_array[MAX_ROWS][512]以错误的顺序访问二维数组(如@JesperJuhl
所建议的那样,循环遍历行,而不是内部循环中一行内的列)所得到的结果。如果恶魔般的无能可以证明具有这样尺寸的二维数组是合理的,那么现实世界中的各种无能很容易证明使用错误的访问模式进行循环是合理的。这发生在现实生活中的真实代码中。

如果数组不是那么大,必要时调整循环边界以使用许多不同的页面而不是重复使用相同的几个页面。跨页面的硬件预取(以及/根本)不起作用。预取器可以在每个页面中跟踪一个前向和一个后向流(这就是这里发生的情况),但只有在内存带宽尚未因非预取而饱和时才会对其进行操作。

这也会产生大量的 TLB 未命中,除非页面被合并到一个大页面中(Linux
机会主义地为匿名(非文件支持)分配,如malloc/new使用mmap(MAP_ANONYMOUS)
)。

您可以使用链表 来代替存储结果列表的数组。每次迭代都需要一个指针追踪加载(下一次加载的加载地址的 RAW
真正依赖风险)。使用错误的分配器,您可能会设法将列表节点分散在内存中,从而破坏缓存。使用糟糕的玩具分配器,它可以将每个节点放在自己页面的开头。(例如,直接分配mmap(MAP_ANONYMOUS),无需分解页面或跟踪对象大小以正确支持free)。


这些并不是真正特定于微体系结构的,并且与流水线关系不大(其中大部分也会导致非流水线 CPU 的速度变慢)。

有点离题:让编译器生成更差的代码/做更多的工作:

使用 C++11std::atomic<int>std::atomic<double>最简单的代码。即使没有来自另一个线程的争用, MFENCE
locked 指令也非常慢。

-m32会使代码变慢,因为 x87 代码会比 SSE2 代码差。基于堆栈的 32 位调用约定需要更多指令,甚至将堆栈上的 FP
参数传递给exp(). atomic<uint64_t>::operator++on-m32需要一个lock cmpxchg8B循环(i586)。(所以将其用于循环计数器![邪恶的笑声])。

-march=i386也会感到悲观(感谢@Jesper)。FPfcom比 686 慢fcomi。586 之前的版本不提供原子 64
位存储(更不用说 cmpxchg),因此所有 64 位atomic操作都编译为 libgcc 函数调用(可能是为 i686
编译的,而不是实际使用锁)。在最后一段中的 Godbolt Compiler Explorer 链接上尝试一下。

在 sizeof( ) 为 10 或 16 的 ABI 中使用long double/
sqrtl/expl来获得额外的精度和额外的缓慢long double(使用填充对齐)。(IIRC,64 位 Windows 使用8
字节long double等效于double。(无论如何,10 字节(80 位)FP 操作数的加载/存储是 4 / 7 uop,而
/或float每个double只需要 1 uop )。强制 x87甚至会失败自动矢量化海湾合作委员会。fld m64/m32``fst``long double``-m64 -march=haswell -O3

如果不使用atomic<uint64_t>循环计数器,请使用long double所有内容,包括循环计数器。

atomic<double>``+=编译,但它不支持 读取-修改-写入操作(即使在 64 位上)。atomic<long double>必须为原子加载/存储调用库函数。这可能真的效率低下,因为 x86 ISA 不自然地支持原子 10
字节加载/存储,而我能想到的唯一没有锁定 ( cmpxchg16b) 的方法需要 64 位模式。


-O0,通过将部分分配给临时变量来分解大表达式将导致更多的存储/重新加载。没有volatile或其他东西,这与真实代码的真实构建将使用的优化设置无关。

C 别名规则允许 achar对任何内容进行别名,因此通过 a存储char*会强制编译器在字节存储之前/之后存储/重新加载所有内容,即使在-O3. (例如,对于在的数组上运行uint8_t的自动矢量化代码来说,这是一个问题。)

尝试uint16_t循环计数器,强制截断为 16 位,可能通过使用 16 位操作数大小(潜在的停顿)和/或额外的movzx指令(安全)。
有符号溢出是未定义的行为,因此除非您使用-fwrapv或至少-fno-strict- overflow,有符号循环计数器不必在每次迭代时重新进行符号扩展,即使用作 64 位指针的偏移量也是如此。


强制从整数float来回转换。和/或double<=>float转换。指令的延迟 > 1,标量 int->float ( cvtsi2ss)
设计不当,无法将 xmm 寄存器的其余部分归零。pxor(出于这个原因,gcc 插入了一个额外的来打破依赖关系。)


经常 将您的 CPU 亲和性设置为不同的 CPU (@Egwor
建议)。恶魔推理:您不希望一个内核因长时间运行线程而过热,是吗?也许换到另一个核心会让那个核心加速到更高的时钟速度。(实际上:它们的热度非常接近,除非在多插槽系统中,否则这种情况极不可能发生)。现在只是弄错了调音并且经常这样做。除了花费在操作系统保存/恢复线程状态的时间之外,新内核还具有冷
L2/L1 缓存、uop 缓存和分支预测器。

引入频繁的不必要的系统调用会减慢您的速度,无论它们是什么。虽然一些重要但简单的,比如gettimeofday可以在用户空间中实现,而不会转换到内核模式。(Linux
上的 glibc 在内核的帮助下做到了这一点:内核在 VDSO 中导出代码+数据)。

有关系统调用开销的更多信息(包括返回用户空间后的缓存/TLB 未命中,而不仅仅是上下文切换本身),FlexSC
论文
对当前情况进行了一些出色的性能计数器分析,以及对批处理系统的建议来自大规模多线程服务器进程的调用。

2022-04-06