首页 文章

取消优化Intel Sandybridge系列CPU中管道的程序

提问于
浏览
299

我一直在绞尽脑汁想要完成这项任务一周,我希望有人能带领我走向正确的道路 . 让我从教师的指示开始:

您的任务与我们的第一个实验任务相反,即优化素数计划 . 你在这个任务中的目的是使程序失望,即让它运行得更慢 . 这两个都是CPU密集型程序 . 他们需要几秒钟才能在我们的实验室电脑上运行 . 您可能无法更改算法 . 要取消优化程序,请使用您对英特尔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;
}

我所做的更改似乎将代码运行时间增加了一秒,但我不完全确定在不添加代码的情况下我可以更改以停止管道 . 指向正确的方向将是非常棒的,我感谢任何回应 .


更新:执行此任务的教授发布了一些细节

亮点是:

  • 这是社区学院的第二学期建筑课(使用轩尼诗和帕特森教科书) .

  • 实验室计算机有Haswell CPU

  • 学生们已接触到 CPUID 指令以及如何确定缓存大小,以及内在函数和 CLFLUSH 指令 .

  • 允许任何编译器选项,因此是内联asm .

  • 编写自己的平方根算法被宣布为在苍白之外

Cowmoogun对元线程的评论表明it wasn't clear compiler optimizations could be part of this, and assumed -O0,运行时间增加17%是合理的 .

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


请记住,这是一个计算机架构问题,而不是关于如何使C缓慢的问题 .

4 回答

  • 31

    迟到的答案,但我觉得我们没有滥用链接列表和TLB .

    使用mmap分配节点,这样您大多使用地址的MSB . 这应该导致长TLB查找链,页面是12位,留下52位翻译,或每次必须遍历的5个级别 . 运气好的话,他们必须每次进入内存进行5级查询加上1次内存访问才能进入你的节点,最高级别很可能是在某个地方的缓存中,所以我们希望有5 *内存访问 . 放置节点,使其跨越最差的边界,以便读取下一个指针将导致另外3-4次翻译查找 . 由于大量的翻译查找,这也可能完全破坏缓存 . 此外,虚拟表的大小可能会导致大多数用户数据被分页到磁盘额外的时间 .

    从单个链表中读取时,请务必每次从列表的开头读取,以便在读取单个数字时造成最大延迟 .

  • 385

    重要背景阅读: Agner Fog's microarch pdf ,也可能是Ulrich Drepper的What Every Programmer Should Know About Memory . 另请参阅x86标记wiki中的其他链接,尤其是Intel 's optimization manuals, and David Kanter' s analysis of the Haswell microarchitecture, with diagrams .

    很酷的任务;比我看到的那些students were asked to optimize some code for gcc -O0要好得多,学习一堆技巧,让我们了解CPU管道并用它来指导你的去优化工作,而不仅仅是盲目的猜测 . The most fun part of this one is justifying each pessimization with "diabolical incompetence", not intentional malice.


    Problems with the assignment wording and code

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

    I'd love to see an answer that attempted to get a slowdown from re-arranging the expressions to change the dependencies, to reduce ILP just from dependencies (hazards). 我没有尝试过 .

    英特尔Sandybridge系列CPU是积极的无序设计,它们需要花费大量的晶体管和电源才能找到并行性,并避免可能会造成麻烦的危险(依赖性)a classic RISC in-order pipeline . 通常,减慢它的唯一传统危险是RAW "true"依赖性,导致吞吐量受到延迟的限制 .

    WAR and WAW hazards for registers are pretty much not an issue, thanks to register renaming . ( popcnt / lzcnt / tzcnt 除外,它有false dependency their destination on Intel CPUs,即使它是只写的 . 即WAW被视为RAW危险的写入) . 对于内存排序,现代CPU使用store queues to delay commit into cache until retirement, also avoiding WAR and WAW hazards .

    Why does mulss take only 3 cycles on Haswell, different from Agner's instruction tables?有更多关于寄存器重命名和隐藏FP点产品循环中的FMA延迟的信息 .


    The "i7" brand-name was introduced with Nehalem (successor to Core2) ,一些英特尔手册甚至说"Core i7",当它们似乎意味着Nehalem,但他们保留了"i7"品牌for Sandybridge以及后来的微架构 . SnB is when the P6-family evolved into a new species, the SnB-family . 在许多方面,Nehalem与Pentium III的共同点多于Sandybridge(例如寄存器读取停顿和ROB读取停顿不会发生在SnB上,因为它改为使用物理寄存器文件 . 还有一个uop缓存和一个不同的内部uop格式) . The term "i7 architecture" is not useful ,因为将SnB系列与Nehalem分组而不是Core2是没有意义的 . (Nehalem确实介绍了共享的包含L3缓存架构,用于将多个内核连接在一起 . 而且还集成了GPU . 因此芯片级别的命名更有意义 . )


    恶魔般无能为力的好主意总结

    即使是恶魔无能也不太可能添加明显无用的工作或无限循环,并且使C / Boost类混乱超出了赋值的范围 .

    • 具有单个共享 std::atomic<uint64_t> 循环计数器的多线程,因此发生了正确的迭代总数 . 原子uint64_t特别糟糕 -m32 -march=i586 . 对于奖励积分,安排它不对齐,并跨越页面边界与不均匀的分裂(不是4:4) .

    • False sharing 用于其他一些非原子变量 - >内存顺序错误推测管道清除,以及额外的缓存未命中 .

    • 而不是在FP变量上使用 - ,用0x80对高字节进行异或,以翻转符号位,从而导致 store-forwarding stalls .

    • 每次迭代的时间独立,甚至比 RDTSC 更重 . 例如 CPUID / RDTSC 或进行系统调用的时间函数 . 序列化指令本质上是管道不友好的 .

    • 更改乘以常数除以它们的倒数("for ease of reading") . div is slow and not fully pipelined.

    • 使用AVX(SIMD)将乘法/ sqrt矢量化,但在调用标量数学库 exp()log() 函数之前无法使用 vzeroupper ,从而导致 AVX<->SSE transition stalls .

    • 将RNG输出存储在链接列表中,或存储在无序遍历的数组中 . 每次迭代的结果相同,并在结束时求和 .

    这个答案也包含在内,但是从摘要中排除:在非流水线CPU上的建议速度一样慢,或者即使在恶魔无能的情况下似乎也不合理 . 例如很多gimp-the-compiler的想法会产生明显不同/更差的asm .


    多线程严重

    也许使用OpenMP来进行多线程循环,迭代次数很少,而且开销比速度增益更多 . 你的monte-carlo代码有足够的并行性来实际获得加速,尤其是 . 如果我们成功地使每次迭代变慢 . (每个线程计算一个部分 payoff_sum ,最后添加) . 该循环上的 #omp parallel 可能是优化,而不是悲观 .

    Multi-thread but force both threads to share the same loop counter (with atomic increments so the total number of iterations is correct). 这似乎是恶魔般的逻辑 . 这意味着使用 static 变量作为循环计数器 . 这证明使用 atomic 用于循环计数器,并创建实际cache-line ping-ponging(只要线程不在具有超线程的相同物理核心上运行;这可能不会那么慢) . 无论如何,这比 lock inc 的无争议案例要慢得多 . 并且 lock cmpxchg8b 以原子方式递增32位系统上的争用 uint64_t 将不得不在循环中重试而不是让硬件仲裁原子 inc .

    还要创建 false sharing ,其中多个线程将其私有数据(例如RNG状态)保存在同一缓存行的不同字节中 . (Intel tutorial about it, including perf counters to look at) . There's a microarchitecture-specific aspect to this :英特尔CPU推测内存错误排序没有发生,并且有一个memory-order machine-clear perf event to detect this, at least on P4 . 哈斯韦尔的罚款可能不会那么大 . 正如该链接所指出的那样, lock ed指令假设会发生这种情况,从而避免错误推测 . 正常负载推测其他核心不会在加载执行和按程序顺序退出之间使缓存行无效(unless you use pause) . 没有 lock ed指令的真正共享通常是一个错误 . 将非原子共享循环计数器与原子情况进行比较会很有趣 . 要真正保持悲观,请保留共享原子循环计数器,并在相同或不同的高速缓存行中导致其他变量的错误共享 .


    随机特定于uarch的想法:

    如果你可以引入 any unpredictable branches ,那将大大减少代码 . 现代x86 CPU具有相当长的流水线,因此误预测需要大约15个周期(从uop缓存运行时) .


    依赖链:

    我认为这是作业的预期部分之一 .

    击败CPU 's ability to exploit instruction-level parallelism by choosing an order of operations that has one long dependency chain instead of multiple short dependency chains. Compilers aren' t允许更改FP计算的操作顺序,除非您使用 -ffast-math ,因为这可能会改变结果(如下所述) .

    要真正使其有效,请增加循环携带的依赖关系链的长度 . 但是,没有什么能够明显地突然出现:所写的循环具有非常短的循环携带依赖链:只是FP添加 . (3个周期) . 多次迭代可以同时在飞行中进行计算,因为它们可以在前一次迭代结束时的 payoff_sum += 之前开始 . ( log()exp 需要很多指令,但不会超过Haswell's out-of-order window for finding parallelism: ROB size=192 fused-domain uops, and scheduler size=60 unfused-domain uops . 一旦当前迭代的执行进展到足以为下一次迭代发出的指令腾出空间,它的任何部分都有其输入就绪(即当较旧的指令使执行单元空闲时(例如,因为它们在延迟而非吞吐量方面存在瓶颈),可以开始执行独立/单独的dep链) .

    RNG状态几乎肯定是比 addps 更长的循环携带依赖链 .


    使用较慢/更多FP操作(尤其是更多分区):

    除以2.0而不是乘以0.5,依此类推 . FP乘法在英特尔设计中是大量流水线,并且在Haswell及更高版本中每0.5c吞吐量有一个 . FP divsd/divpd is only partially pipelined . (尽管Skylake的每4c吞吐量令人印象深刻,但是延迟时间为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) . 请注意,虽然实数上的数学是关联的,floating point math is not,即使不考虑溢出/ NaN(这就是默认情况下 -ffast-math 未启用的原因) . 请参阅Paul's comment以获取非常多毛的嵌套 pow() 建议 .

    如果您可以将计算缩小到非常小的数字,则FP数学运算采用 ~120 extra cycles to trap to microcode when an operation on two normal numbers produces a denormal . 看到Agner Fog 's microarch pdf for the exact numbers and details. This is unlikely since you have a lot of multiplies, so the scale factor would be squared and underflow all the way to 0.0. I don'看到任何方式来证明必要的缩放与无能(甚至恶魔),只有故意的恶意 .


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

    Use movnti to evict your data from cache . 恶魔:它是新的和弱排序的,所以应该让CPU更快地运行它,对吧?或者看一下这样一个案例的链接问题:某人有可能做到这一点(对于只有部分位置很热的分散写入) . 没有恶意, clflush 可能是不可能的 .

    在FP数学运算之间使用整数混洗以导致旁路延迟 .

    Mixing SSE and AVX instructions without proper use of vzeroupper causes large stalls in pre-Skylake (以及不同的处罚in Skylake) . 即使没有它,矢量化也可能比标量更糟糕(将数据移入或移出矢量所花费的周期多于通过一次执行4次蒙特卡罗迭代的add / sub / mul / div / sqrt操作所保存的,使用256b矢量) . add / sub / mul执行单元是完全流水线和全宽的,但256b向量上的div和sqrt对于 double 来说并不显着 .

    exp()log() 没有硬件支持,因此该部分需要将向量元素提取回标量并分别调用库函数,然后将结果混洗回向量 . libm通常编译为仅使用SSE2,因此将使用标量数学指令的legacy-SSE编码 . 如果您的代码使用256b向量并且在没有首先执行 vzeroupper 的情况下调用 exp ,那么您将停止 . 返回后,像 vmovsd 这样的AVX-128指令将下一个向量元素设置为 exp 的arg也将停止 . 然后 exp() 在运行SSE指令时将再次停止 . This is exactly what happened in this question, causing a 10x slowdown. (谢谢@ZBoson) .

    另见Nathan Kurz's experiments with Intel's math lib vs. glibc for this code . 未来的glibc将伴随vectorized implementations of exp() and so on.


    如果定位前IvB,或特别是Nehalem,试图让gcc导致16位或8位操作的部分寄存器停顿,然后是32位或64位操作 . 在大多数情况下,gcc将在8或16位操作后使用 movzx ,但here's a case where gcc modifies ah and then reads ax


    使用(内联)asm:

    使用(内联)asm,您可以打破uop缓存:32B块代码不适合三个6uop缓存行,强制从uop缓存切换到解码器 . 在内部循环内的分支目标上使用许多单字节 nop 而不是几个长 nop 的无能 ALIGN 可能会起作用 . 或者将对齐填充放在标签之后,而不是之前 . :P这只有在前端是瓶颈的情况下才有意义,如果我们成功地对其余的代码进行了粗略化,那么它就不会出现这种情况 .

    使用自修改代码来触发管道清除(也称为机器核武器) .

    来自16位指令的LCP stalls具有太大以至于不能适合8位的中间值,因此不太可能有用 . SnB上的uop缓存以及后来意味着您只需支付一次解码惩罚 . 在Nehalem(第一个i7)上,它可能适用于不适合28 uop循环缓冲区的循环 . gcc有时会生成这样的指令,即使使用 -mtune=intel ,也可能使用32位指令 .


    A common idiom for timing is CPUID(to serialize) then RDTSC . 使用 CPUID / RDTSC 分别计算每次迭代的时间,以确保 RDTSC 没有使用先前的指令重新排序,这将减慢很多事情 . (在现实生活中,聪明的时间方法是将所有迭代计时在一起,而不是分别计时并将它们相加) .


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

    对某些变量使用 union { double d; char a[8]; } . Cause a store-forwarding stall通过对其中一个字节进行窄存储(或读 - 修改 - 写) . (该wiki文章还涵盖了许多其他用于加载/存储队列的微体系结构内容) . 例如 flip the sign of a double using XOR 0x80 on just the high byte ,而不是 - 运算符 . 恶魔无能的开发人员可能听说FP比整数慢,因此尽量使用整数运算来做 . (在SSE寄存器中针对FP数学的一个非常好的编译器可能会将其编译为 xorps ,并在另一个xmm寄存器中使用常量,但这是n = 't terrible for x87 is if the compiler realizes that it'否定该值并用减法替换下一个add的唯一方法 . )


    如果您正在使用 -O3 进行编译而不使用 std::atomic ,请使用 volatile ,以强制编译器实际存储/重新加载到处 . 全局变量(而不是本地变量)也会强制执行某些存储/重新加载,但the C++ memory model's weak ordering不需要编译器一直溢出/重新加载到内存 .

    Replace local vars with members of a big struct, so you can control the memory layout.

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

    选择你的内存布局,以便everything goes into a different line in the same "set" in the L1 cache . 它只有8路联想,即每组有8个"ways" . 缓存行为64B .

    更好的是, put things exactly 4096B apart, since loads have a false dependency on stores to different pages but with the same offset within a page . 积极的无序CPU使用Memory Disambiguation to figure out when loads and stores can be reordered without changing the results和英特尔's implementation has false-positives that prevent loads from starting early. Probably they only check bits below the page offset, so the check can start before the TLB has translated the high bits from a virtual page to a physical page. As well as Agner'指南,请参阅an answer from Stephen Canon,以及@Krazy Glew 's answer on the same question. (Andy Glew was one of the architects of Intel' s原始P6微体系结构附近的部分 . )

    使用 __attribute__((packed)) 可以让您错误地对齐变量,使它们跨越缓存行甚至页面边界 . (因此,一个 double 的负载需要来自两个缓存行的数据) . 除非跨越缓存行和页面行,否则未对齐的负载在任何Intel i7 uarch中都不会受到惩罚 . Cache-line splits still take extra cycles . Skylake大大减少了页面拆分负载的惩罚,from 100 to 5 cycles. (Section 2.1.3) . 也许与能够并行执行两个页面步行有关 .

    A page-split on an atomic<uint64_t> should be just about the worst case ,尤其是如果它在一个页面中是5个字节而在另一个页面中是3个字节,或者是4:4以外的任何其他字节 . 对于某些搜索IIRC上的16B向量的高速缓存行分割,甚至在中间分割也更有效 . 将所有内容放入 alignas(4096) struct __attribute((packed)) (当然为了节省空间),包括用于存储RNG结果的数组 . 通过在计数器之前使用 uint8_tuint16_t 来实现错位 .

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

    如果你可以引入额外的间接级别,那么早期就不知道加载/存储地址,这可能会进一步降低 .


    以非连续顺序遍历数组

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

    对于“最大随机性”,我们可以在随机数组上循环一个线程,将新的随机数写入其中 . 消耗随机数的线程可以生成随机索引以从中加载随机数 . (这里有一些make-work,但是在体系结构上它有助于早期知道加载地址,因此在需要加载数据之前可以解决任何可能的加载延迟 . )在不同的内核上使用读写器将导致内存排序错误-speculation管道清除(如前面讨论的假共享案例) .

    为了获得最大的悲观,请以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] 这样的2D数组(循环遍历行,而不是内循环中一行内的列,如@JesperJuhl所建议的那样) . 如果恶魔般的无能可以证明具有这种尺寸的二维阵列是合理的,那么花园种类的现实世界的无能就很容易证明用错误的访问模式进行循环 . 这发生在现实生活中的真实代码中 .

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

    除非页面合并到一个巨大的页面(Linux does this opportunistically for anonymous (not file-backed) allocations like malloc/new that use mmap(MAP_ANONYMOUS)),否则这也会产生大量的TLB未命中 .

    您可以使用 linked list 而不是存储结果列表的数组 . 然后每次迭代都需要一个指针追逐加载(对于下一个加载的加载地址,RAW真正的依赖性危险) . 使用错误的分配器,您可能会设法将列表节点分散到内存中,从而破坏缓存 . 使用恶魔般无能的分配器,它可以将每个节点放在其自己的页面的开头 . (例如,直接分配 mmap(MAP_ANONYMOUS) ,不分解页面或跟踪对象大小以正确支持 free ) .


    这些并不是特定于微体系结构的,并且与管道几乎没有关系(其中大多数也是非流水线CPU的减速) .

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

    使用C 11 std::atomic<int>std::atomic<double> 作为最多的代码 . 即使没有来自另一个线程的争用,MFENCE和 lock ed指令也非常慢 .

    -m32 将使代码变慢,因为x87代码将比SSE2代码更糟糕 . 基于堆栈的32位调用约定需要更多指令,并且甚至将堆栈上的FP args传递给 exp() 等函数 . atomic<uint64_t>::operator++ on -m32 requires a lock cmpxchg8B loop(i586) (所以用它来循环计数器![邪笑]) .

    -march=i386 也会悲观(感谢@Jesper) . FP与 fcom 的比较慢于686 fcomi . Pre-586不提供原子64位存储(更不用说cmpxchg)了,所以所有64位 atomic ops都编译成libgcc函数调用(可能是为i686编译的,而不是实际使用锁) . 在最后一段中的Godbolt Compiler Explorer链接上尝试一下 .

    使用 long double / sqrtl / expl 用于ABI中的额外精度和额外慢度,其中sizeof( long double )为10或16(对齐用填充) . (IIRC,64位Windows使用8byte long double 等效于 double . (无论如何,10字节(80位)FP操作数的加载/存储是4/7 uops,而 floatdouble 仅为 fld m64/m32 / fst 取1 uop . )强制x87与 long double 即使对于gcc -m64 -march=haswell -O3 也会失败自动矢量化 .

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

    atomic<double> 编译,但不支持 += 之类的读 - 修改 - 写操作(即使在64位上) . atomic<long double> 必须仅为原子加载/存储调用库函数 . 它可能效率很低,because the x86 ISA doesn't naturally support atomic 10byte loads/stores,我能想到没有锁定的唯一方法( cmpxchg16b )需要64位模式 .


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

    C别名规则允许 char 为别名,因此通过 char* 存储会强制编译器在字节存储之前/之后存储/重新加载所有内容,即使在 -O3 . (例如,这是自动矢量化code that operates on an array of uint8_t的问题 . )

    尝试 uint16_t 循环计数器,强制截断到16位,可能是使用16位操作数大小(潜在停顿)和/或额外 movzx 指令(安全) . Signed overflow is undefined behaviour,所以除非你使用 -fwrapv 或至少 -fno-strict-overflowsigned loop counters don't have to be re-sign-extended every iteration,即使用作64位指针的偏移量 .


    强制从整数转换为 float 并再次返回 . 和/或 double <=> float 次转化 . 这些指令具有大于一个的延迟,并且标量int-> float( cvtsi2ss )的设计非常糟糕,不能将xmm寄存器的其余部分归零 . (出于这个原因,gcc会插入一个额外的 pxor 来破坏依赖关系 . )


    经常 set your CPU affinity to a different CPU (@Egwor建议) . 恶魔般的推理:你们彼此之间的热度非常接近,除非在多插座系统中,这种情况极不可能 . 现在只是调整错误并经常这样做 . 除了在OS保存/恢复线程状态中花费的时间之外,新内核还具有冷L2 / L1高速缓存,uop高速缓存和分支预测器 .

    频繁引入不必要的系统调用会降低您的速度,无论它们是什么 . 虽然像 gettimeofday 这样的一些重要但简单的可能会在用户空间中实现,而不会转换到内核模式 . (Linux上的glibc在内核的帮助下完成此操作,因为内核在_162691中导出代码) .

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

  • 10

    您可以采取一些措施使事情尽可能地糟糕:

    • 编译i386架构的代码 . 这将阻止使用SSE和更新的指令并强制使用x87 FPU .

    • 到处使用 std::atomic 变量 . 由于编译器被迫在整个地方插入内存屏障,这将使它们非常昂贵 . 这是一个无能为力的人可能合理地做的事情"ensure thread safety" .

    • 确保以最糟糕的方式访问内存以供预取程序预测(列主要与行主要) .

    • 为了使你的变量更加昂贵你可以通过用 new 分配它们来确保它们都有'dynamic storage duration'(堆分配),而不是让它们有'automatic storage duration'(堆栈已分配) .

    • 确保您分配的所有内存都非常奇怪地对齐,并且无论如何都要避免分配大页面,因为这样做会使TLB效率太高 .

    • 无论你做什么,都要让代码运行得更慢,但它会浪费一些额外的磁盘空间 .

    注意:这个答案基本上只是总结了我对@Peter Cordes已经纳入他非常好的答案的评论 . 建议如果你只有一个人可以得到你的赞成:)

  • 3

    您可以使用 long double 进行计算 . 在x86上,它应该是80位格式 . 只有传统的x87 FPU才支持此功能 .

    x87 FPU的缺点很少:

    • 缺少SIMD,可能需要更多说明 .

    • 基于堆栈,对于超标量和流水线架构存在问题 .

    • 单独和非常小的寄存器集,可能需要从其他寄存器和更多内存操作进行更多转换 .

    • 在Core i7上有3个SSE端口和2个x87端口,处理器可以执行较少的并行指令 .

相关问题