Логарифм в С++ и сборка

По-видимому, 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);
}