C++ 和汇编中的对数
Logarithm in C++ and assembly
显然 MSVC++2017 工具集 v141(x64 发布配置)不通过 C/C++ 内部函数使用 FYL2X
x86_64 汇编指令,而是使用 C++ log()
或 log2()
用法导致对长函数的实际调用,该函数似乎实现了对数的近似值(不使用 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;
}
Ryzen 1800X 的输出是:
Log2: 95152910.728 Ops/sec calculated 2513272986.435
Ln: 168109607.464 Ops/sec calculated 1742068084.525
所以为了阐明这些现象(没有使用 FYL2X
和奇怪的性能差异),我还想测试 FYL2X
的性能,如果它更快,用它代替 <cmath>
的函数。 MSVC++ 不允许在 x64 上进行内联汇编,因此需要一个使用 FYL2X
的汇编文件函数。
你能用这样一个函数的汇编代码来回答吗,它使用 FYL2X
或更好的对数指令(不需要特定的基数),如果在较新的 x86_64 处理器上有的话?
这里是使用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 ,例如
The x87 register stack is unused. It may be used by the callee, but
must be considered volatile across function calls.
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);
}
显然 MSVC++2017 工具集 v141(x64 发布配置)不通过 C/C++ 内部函数使用 FYL2X
x86_64 汇编指令,而是使用 C++ log()
或 log2()
用法导致对长函数的实际调用,该函数似乎实现了对数的近似值(不使用 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;
}
Ryzen 1800X 的输出是:
Log2: 95152910.728 Ops/sec calculated 2513272986.435
Ln: 168109607.464 Ops/sec calculated 1742068084.525
所以为了阐明这些现象(没有使用 FYL2X
和奇怪的性能差异),我还想测试 FYL2X
的性能,如果它更快,用它代替 <cmath>
的函数。 MSVC++ 不允许在 x64 上进行内联汇编,因此需要一个使用 FYL2X
的汇编文件函数。
你能用这样一个函数的汇编代码来回答吗,它使用 FYL2X
或更好的对数指令(不需要特定的基数),如果在较新的 x86_64 处理器上有的话?
这里是使用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 ,例如
The x87 register stack is unused. It may be used by the callee, but must be considered volatile across function calls.
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);
}