Логарифм в C++ и сборка
Очевидно MSVC++2017 toolset v141 (конфигурация выпуска x64) не использует FYL2X
Инструкция по сборке x86_64 через собственный C/C++, а точнее C++ log()
или же log2()
использование приводит к реальному вызову длинной функции, которая, кажется, реализует приближение логарифма (без использования FYL2X
). Производительность, которую я измерил, тоже странная: log()
(натуральный логарифм) в 1,7667 раза быстрее, чем log2()
(логарифм по основанию 2), хотя логарифм по основанию 2 должен быть проще для процессора, поскольку он хранит показатель степени в двоичном формате (и мантиссе тоже), и поэтому кажется, что инструкция процессора 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?
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 не используется. Он может использоваться вызываемым абонентом, но должен рассматриваться как нестабильный при вызовах функций.
Прототип в C++ это:
extern "C" double __fastcall SRLog2MulD(const double toLog, const double toMul);
Производительность в 2 раза медленнее, чем std::log2()
и более чем в 3 раза медленнее, чем std::log()
:
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);
}