Логарифм в С++ и сборка
По-видимому, MSVС++ 2017 toolset v141 (конфигурация релиза x64) не использует инструкцию сборки FYL2X
x86_64 с помощью встроенного C/С++, но вместо того, чтобы использовать С++ log()
или log2()
, возникает реальный вызов которая, по-видимому, реализует приближение логарифма (без использования FYL2X
). Производительность, которую я измерил, также странная: log()
(натуральный логарифм) в 1.7667 раз быстрее, чем log2()
(логарифм базы 2), хотя логарифм базы 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>
. MSVС++ не разрешает встроенную сборку на 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 не используется. Он может использоваться вызываемым лицом, но должен считаться изменчивым во всех вызовах функций.
Прототипом в С++ является:
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);
}