Логарифм в 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);
}
Другие вопросы по тегам