C++ 和汇编中的对数

Posted

技术标签:

【中文标题】C++ 和汇编中的对数【英文标题】:Logarithm in C++ and assembly 【发布时间】:2017-08-20 18:46:46 【问题描述】:

显然 MSVC++2017 工具集 v141(x64 发布配置)不通过 C/C++ 内在函数使用 FYL2X x86_64 汇编指令,而是 C++ log()log2() 用法导致对long 函数,它似乎实现了对数的近似(不使用FYL2X)。我测量的性能也很奇怪:log()(自然对数)比log2()(以 2 为底的对数)快 1.7667 倍,尽管以 2 为底的对数对处理器来说应该更容易,因为它以二进制格式存储指数(并且尾数),这似乎是 CPU 指令 FYL2X 计算以 2 为底的对数(乘以参数)的原因。

这是用于测量的代码:

#include <chrono>
#include <cmath>
#include <cstdio>

const int64_t cnLogs = 100 * 1000 * 1000;

void BenchmarkLog2() 
  double sum = 0;
  auto start = std::chrono::high_resolution_clock::now();
  for(int64_t i=1; i<=cnLogs; i++) 
    sum += std::log2(double(i));
  
  auto elapsed = std::chrono::high_resolution_clock::now() - start;
  double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
  printf("Log2: %.3lf Ops/sec calculated %.3lf\n", cnLogs / nSec, sum);


void BenchmarkLn() 
  double sum = 0;
  auto start = std::chrono::high_resolution_clock::now();
  for (int64_t i = 1; i <= cnLogs; i++) 
    sum += std::log(double(i));
  
  auto elapsed = std::chrono::high_resolution_clock::now() - start;
  double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
  printf("Ln: %.3lf Ops/sec calculated %.3lf\n", cnLogs / nSec, sum);


int main() 
    BenchmarkLog2();
    BenchmarkLn();
    return 0;

锐龙 1800X 的输出是:

Log2: 95152910.728 Ops/sec calculated 2513272986.435
Ln: 168109607.464 Ops/sec calculated 1742068084.525

所以为了阐明这些现象(没有使用FYL2X和奇怪的性能差异),我还想测试FYL2X的性能,如果它更快,请使用它来代替&lt;cmath&gt;的功能。 MSVC++ 不允许在 x64 上进行内联汇编,因此需要使用 FYL2X 的汇编文件函数。

如果在较新的 x86_64 处理器上有任何功能,您能否回答使用 FYL2X 或更好的对数指令(无需特定基数)的此类函数的汇编代码?

【问题讨论】:

您在寻找什么样的准确性/速度权衡? FYL2X 并不快,尽管在 Ryzen 上不如在现代 Intel 上那么糟糕。 @harold,我想要 CPU 本身提供的精度/速度,即没有软件近似。如果硬件中有多个选项,我想全部考虑。 登录不同的基数通常是通过将结果乘以一个常数 (log_n(x) = log2(x) * log_n(2)) 来完成的。或者只是将其烘焙到log(mantissa) 的多项式逼近的常数中。例如,请参阅 Agner Fog 的高精度 log(__m256d) 的矢量类库实现以及其他基础的版本。 (单精度版本稍微简单一些,不需要那么大的多项式即可获得接近 1 ulp 的精度)。 有关 SSE2 log2() 的更快近似值,请参阅 jrfonseca.blogspot.ca/2008/09/…。带有 FMA 的多项式的 AVX2 或 AVX512 版本非常有效,即使使用 5 阶多项式使其非常准确。在现代 CPU 上使用 FYL2X 没有意义,除非您针对代码大小而不是速度进行优化。 @PeterCordes 我尝试了一些东西,发现二阶多项式的比率可以比五阶多项式拟合一个数量级,因此以除法为代价节省了 3 个 FMA 的延迟,也许偶尔有用 【参考方案1】:

这是使用FYL2X的汇编代码:

_DATA SEGMENT

_DATA ENDS

_TEXT SEGMENT

PUBLIC SRLog2MulD

; XMM0L=toLog
; XMM1L=toMul
SRLog2MulD PROC
  movq qword ptr [rsp+16], xmm1
  movq qword ptr [rsp+8], xmm0
  fld qword ptr [rsp+16]
  fld qword ptr [rsp+8]
  fyl2x
  fstp qword ptr [rsp+8]
  movq xmm0, qword ptr [rsp+8]
  ret

SRLog2MulD ENDP

_TEXT ENDS

END

调用约定是根据https://docs.microsoft.com/en-us/cpp/build/overview-of-x64-calling-conventions,例如

x87 寄存器堆栈未使用。它可能被被调用者使用,但是 必须在函数调用中被视为 volatile。

C++ 中的原型是:

extern "C" double __fastcall SRLog2MulD(const double toLog, const double toMul);

性能比std::log2()慢2倍,比std::log()慢3倍以上:

Log2: 94803174.389 Ops/sec calculated 2513272986.435
FPU Log2: 52008300.525 Ops/sec calculated 2513272986.435
Ln: 169392473.892 Ops/sec calculated 1742068084.525

基准测试代码如下:

void BenchmarkFpuLog2() 
  double sum = 0;
  auto start = std::chrono::high_resolution_clock::now();
  for (int64_t i = 1; i <= cnLogs; i++) 
    sum += SRPlat::SRLog2MulD(double(i), 1);
  
  auto elapsed = std::chrono::high_resolution_clock::now() - start;
  double nSec = 1e-6 * std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
  printf("FPU Log2: %.3lf Ops/sec calculated %.3lf\n", cnLogs / nSec, sum);

【讨论】:

您在哪个 CPU 上进行了测试?不过,我不希望有任何现代 CPU 的 x87 FYL2X 比准确的 SSE2 库实现更快。这么慢的速度超过了额外的存储/重新加载开销(用于在 xmm 和 x87 寄存器之间移动数据)。 @PeterCordes ,CPU 是 AMD Ryzen 1800X 库存时钟。

以上是关于C++ 和汇编中的对数的主要内容,如果未能解决你的问题,请参考以下文章

C++反汇编第四讲,认识多重继承,菱形继承的内存结构,以及反汇编中的表现形式.

内联汇编中的子数组。 C++

dll文件如何反汇编成源码,C++语言编写

C++反汇编与逆向分析技术揭秘的目录

C++反汇编第三讲,反汇编中识别继承关系,父类,子类,成员对象

使用IDA查看汇编代码上下文去辅助排查C++软件异常问题