Как ускорить генерацию сложных случайных чисел

у меня есть код, который выполняет много журналов,tan и cos операции на парном разряде. Мне нужно, чтобы все произошло как можно быстрее. В настоящее время я использую код, такой как

#include <stdio.h>
#include <stdlib.h>
#include "mtwist.h"
#include <math.h>


int main(void) {
   int i;
   double x;
   mt_seed();
   double u1;
   double u2;
   double w1;
   double w2;
   x = 0;
   for(i = 0; i < 100000000; ++i) {
     u1 = mt_drand();
     u2 = mt_drand();
     w1 = M_PI*(u1-1/2.0);
     w2 = -log(u2);
     x += tan(w1)*(M_PI_2-w1)+log(w2*cos(w1)/(M_PI_2-w1));
   }
   printf("%fn",x); 

   return EXIT_SUCCESS;
}

я использую gcc.

есть два очевидных способа ускорить этот процесс. Первый-выбрать более быстрый RNG. Второе-ускорить трансцендентальные функции.
Для этого я хотел бы знать

  1. как Tan и cos реализованы в сборке на x86? Мой CPU-это AMD FX-8350, если это имеет значение. (Ответ fcos на cos и fptan на tan.)
  2. как вы можете использовать таблицу поиска для ускорения вычислений? Мне нужно только 32 бита точности. Можете ли вы использовать таблицу размера 2^16, например, для ускорения операций tan и cos?

на руководство по оптимизации Intel говорит

если нет критической необходимости оценивать трансцендентальные функции используя расширенную точность 80 бит, приложения должны учитывать альтернативный, основанный на программном обеспечении подход, такой как поиск по таблице алгоритм с использованием методов интерполяции. Можно улучшить трансцендентного исполнения этих методов выбрав требуемая числовая точность и размер таблицы поиска, а также использование параллелизма SSE и SSE2 инструкции.

в соответствии с этим очень полезно стол, fcos имеет задержку 154 и fptan С задержкой 166-231.


вы можете скомпилировать мой код, используя

GCC-O3-стена случайная.c mtwist-1.5 / mtwist.c-lm-o random

мой код C использует код Mersenne Twister RNG C от здесь . Вы должны иметь возможность запустить мой код, чтобы проверить его. Если не можете, пожалуйста, дайте мне знать.


обновление @rhashimoto ускорился мой код от 20 секунд до 6 секунд!

RNG кажется, что это должно быть возможно ускорить. Однако в моих тестах http://www.math.sci.hiroshima-u.ac.jp / ~%20m-mat/MT/SFMT / index.html#dSFMT занимает ровно столько же времени (кто-нибудь видит что-то другое). Если кто-то может найти более быстрый RNG (который проходит все жесткие тесты), я был бы очень благодарен.

пожалуйста, покажите реальные тайминги для любого улучшения вы предлагаете, как это действительно помогает работать что работает или не работает.

8 ответов


вы можете попробовать этот log(x) замена я написал, используя SSE2 встроенные:

#include <assert.h>
#include <immintrin.h>

static __m128i EXPONENT_MASK;
static __m128i EXPONENT_BIAS;
static __m128i EXPONENT_ZERO;
static __m128d FIXED_SCALE;
static __m128d LOG2ERECIP;
static const int EXPONENT_SHIFT = 52;

// Required to initialize constants.
void sselog_init() {
   EXPONENT_MASK = _mm_set1_epi64x(0x7ff0000000000000UL);
   EXPONENT_BIAS = _mm_set1_epi64x(0x00000000000003ffUL);
   EXPONENT_ZERO = _mm_set1_epi64x(0x3ff0000000000000UL);
   FIXED_SCALE = _mm_set1_pd(9.31322574615478515625e-10); // 2^-30
   LOG2ERECIP = _mm_set1_pd(0.693147180559945309417232121459); // 1/log2(e)
}

// Extract IEEE754 double exponent as integer.
static inline __m128i extractExponent(__m128d x) {
   return
      _mm_sub_epi64(
         _mm_srli_epi64(
            _mm_and_si128(_mm_castpd_si128(x), EXPONENT_MASK),
            EXPONENT_SHIFT),
         EXPONENT_BIAS);
}

// Set IEEE754 double exponent to zero.
static inline __m128d clearExponent(__m128d x) {
   return
      _mm_castsi128_pd(
         _mm_or_si128(
            _mm_andnot_si128(
               EXPONENT_MASK,
               _mm_castpd_si128(x)),
            EXPONENT_ZERO));
}

// Compute log(x) using SSE2 intrinsics to >= 30 bit precision, except denorms.
double sselog(double x) {
   assert(x >= 2.22507385850720138309023271733e-308); // no denormalized

   // Two independent logarithms could be computed by initializing
   // base with two different values, either with independent
   // arguments to _mm_set_pd() or from contiguous memory with
   // _mm_load_pd(). No other changes should be needed other than to
   // extract both results at the end of the function (or just return
   // the packed __m128d).

   __m128d base = _mm_set_pd(x, x);
   __m128i iLog = extractExponent(base);
   __m128i fLog = _mm_setzero_si128();

   base = clearExponent(base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   // fLog = _mm_slli_epi64(fLog, 10); // Not needed first time through.
   fLog = _mm_or_si128(extractExponent(base), fLog);

   base = clearExponent(base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   fLog = _mm_slli_epi64(fLog, 10);
   fLog = _mm_or_si128(extractExponent(base), fLog);

   base = clearExponent(base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   base = _mm_mul_pd(base, base);
   fLog = _mm_slli_epi64(fLog, 10);
   fLog = _mm_or_si128(extractExponent(base), fLog);

   // No _mm_cvtepi64_pd() exists so use _mm_cvtepi32_pd() conversion.
   iLog = _mm_shuffle_epi32(iLog, 0x8);
   fLog = _mm_shuffle_epi32(fLog, 0x8);

   __m128d result = _mm_mul_pd(_mm_cvtepi32_pd(fLog), FIXED_SCALE);
   result = _mm_add_pd(result, _mm_cvtepi32_pd(iLog));

   // Convert from base 2 logarithm and extract result.
   result = _mm_mul_pd(result, LOG2ERECIP);
   return ((double *)&result)[0]; // other output in ((double *)&result)[1]
}

код реализует алгоритм, описанный в это Texas Instruments краткое, неоднократно квадратуры аргумент и обьединении степени бит. Это будет не работа с ненормированным входов. Он обеспечивает по крайней мере 30 битов точности.

это работает быстрее, чем log() на одной из моих машин и медленнее на других, так что ваш пробег может варьироваться; Я не утверждаю, что это обязательно лучший подход. Этот код, однако, фактически вычисляет два логарифма параллельно, используя обе половины 128-битного слова SSE2 (хотя функция as-is возвращает только один результат), поэтому он может быть адаптирован в один строительный блок вычисления SIMD всей вашей функции (и я думаю log является сложной частью, как cos довольно хорошо себя ведет). Кроме того, ваш процессор поддерживает AVX, который может упаковать 4 элемента двойной точности в 256-битное слово и расширение этого кода на AVX должно быть простым.

если вы решите не идти полным SIMD, вы все равно можете использовать оба слота логарифма по конвейерной линии-т. е. вычислить log(w2*cos(w1)/(M_PI_2-w1)) на текущей итерации с log(u2) на далее итерации.

даже если эта функция тестирует медленнее против log в изоляции, это все еще может стоить тестирования с вашей фактической функцией. Этот код не подчеркивает кэш данных вообще, поэтому он может быть более дружественным с другими код, который делает (например, с cos который использует таблицы поиска). Также микроинструкция отправки может быть улучшена (или нет) с окружающим кодом, в зависимости от того, использует ли другой код SSE.

мой другой совет (повторенный из комментариев) будет:

  • попробовать -march=native -mtune=native чтобы получить gcc для оптимизации для вашего процессора.
  • избегайте вызова обоих tan и cos по тому же аргументу-use sincos или тригонометрия.
  • рассмотрите возможность использования GPU (например, OpenCL).

кажется, что лучше вычислить sin вместо cos - причина в том, что вы можете использовать его для tan_w1 = sin_w1/sqrt(1.0 - sin_w1*sin_w1). Используя cos, который я первоначально предложил, теряет правильный знак при вычислении tan. И похоже, что вы можете получить хорошее ускорение, используя минимаксный полином над [-pi/2, pi/2], как сказали другие ответчики. 7-терм функция at этой ссылке (убедитесь, что вы получить minimaxsin, а не taylorsin), кажется, работает весьма неплохо.

Итак, вот ваша программа, переписанная со всеми трансцендентальными приближениями SSE2:

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <immintrin.h>
#include "mtwist.h"

#if defined(__AVX__)
#define VECLEN 4
#elif defined(__SSE2__)
#define VECLEN 2
#else
#error // No SIMD available.
#endif

#if VECLEN == 4
#define VBROADCAST(K) { K, K, K, K };
typedef double vdouble __attribute__((vector_size(32)));
typedef long vlong __attribute__((vector_size(32)));
#elif VECLEN == 2
#define VBROADCAST(K) { K, K };
typedef double vdouble __attribute__((vector_size(16)));
typedef long vlong __attribute__((vector_size(16)));
#endif

static const vdouble FALLBACK_THRESHOLD = VBROADCAST(1.0 - 0.001);

vdouble sse_sin(vdouble x) {
   static const vdouble a0 = VBROADCAST(1.0);
   static const vdouble a1 = VBROADCAST(-1.666666666640169148537065260055e-1);
   static const vdouble a2 = VBROADCAST( 8.333333316490113523036717102793e-3);
   static const vdouble a3 = VBROADCAST(-1.984126600659171392655484413285e-4);
   static const vdouble a4 = VBROADCAST( 2.755690114917374804474016589137e-6);
   static const vdouble a5 = VBROADCAST(-2.502845227292692953118686710787e-8);
   static const vdouble a6 = VBROADCAST( 1.538730635926417598443354215485e-10);

   vdouble xx = x*x;
   return x*(a0 + xx*(a1 + xx*(a2 + xx*(a3 + xx*(a4 + xx*(a5 + xx*a6))))));
}

static inline vlong shiftRight(vlong x, int bits) {
#if VECLEN == 4
   __m128i lo = (__m128i)_mm256_extractf128_si256((__m256i)x, 0);
   __m128i hi = (__m128i)_mm256_extractf128_si256((__m256i)x, 1);
   return (vlong)
      _mm256_insertf128_si256(
         _mm256_castsi128_si256(_mm_srli_epi64(lo, bits)),
         _mm_srli_epi64(hi, bits),
         1);
#elif VECLEN == 2
   return (vlong)_mm_srli_epi64((__m128i)x, bits);
#endif
}

static inline vlong shiftLeft(vlong x, int bits) {
#if VECLEN == 4
   __m128i lo = (__m128i)_mm256_extractf128_si256((__m256i)x, 0);
   __m128i hi = (__m128i)_mm256_extractf128_si256((__m256i)x, 1);
   return (vlong)
      _mm256_insertf128_si256(
         _mm256_castsi128_si256(_mm_slli_epi64(lo, bits)),
         _mm_slli_epi64(hi, bits),
         1);
#elif VECLEN == 2
   return (vlong)_mm_slli_epi64((__m128i)x, bits);
#endif
}

static const vlong EXPONENT_MASK = VBROADCAST(0x7ff0000000000000L);
static const vlong EXPONENT_BIAS = VBROADCAST(0x00000000000003ffL);
static const vlong EXPONENT_ZERO = VBROADCAST(0x3ff0000000000000L);
static const vdouble FIXED_SCALE = VBROADCAST(9.31322574615478515625e-10); // 2^-30
static const vdouble LOG2ERECIP = VBROADCAST(0.6931471805599453094172);
static const int EXPONENT_SHIFT = 52;

// Extract IEEE754 double exponent as integer.
static inline vlong extractExponent(vdouble x) {
   return shiftRight((vlong)x & EXPONENT_MASK, EXPONENT_SHIFT) - EXPONENT_BIAS;
}

// Set IEEE754 double exponent to zero.
static inline vdouble clearExponent(vdouble x) {
   return (vdouble)(((vlong)x & ~EXPONENT_MASK) | EXPONENT_ZERO);
}

// Compute log(x) using SSE2 intrinsics to >= 30 bit precision, except
// denorms.
vdouble sse_log(vdouble base) {
   vlong iLog = extractExponent(base);
   vlong fLog = VBROADCAST(0);

   base = clearExponent(base);
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   fLog = shiftLeft(fLog, 10);
   fLog |= extractExponent(base);

   base = clearExponent(base);
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   fLog = shiftLeft(fLog, 10);
   fLog |= extractExponent(base);

   base = clearExponent(base);
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   base = base*base;
   fLog = shiftLeft(fLog, 10);
   fLog |= extractExponent(base);

   // No _mm_cvtepi64_pd() exists so use 32-bit conversion to double.
#if VECLEN == 4
   __m128i iLogLo = _mm256_extractf128_si256((__m256i)iLog, 0);
   __m128i iLogHi = _mm256_extractf128_si256((__m256i)iLog, 1);
   iLogLo = _mm_srli_si128(_mm_shuffle_epi32(iLogLo, 0x80), 8);
   iLogHi = _mm_slli_si128(_mm_shuffle_epi32(iLogHi, 0x08), 8);

   __m128i fLogLo = _mm256_extractf128_si256((__m256i)fLog, 0);
   __m128i fLogHi = _mm256_extractf128_si256((__m256i)fLog, 1);
   fLogLo = _mm_srli_si128(_mm_shuffle_epi32(fLogLo, 0x80), 8);
   fLogHi = _mm_slli_si128(_mm_shuffle_epi32(fLogHi, 0x08), 8);

   vdouble result = _mm256_cvtepi32_pd(iLogHi | iLogLo) +
      FIXED_SCALE*_mm256_cvtepi32_pd(fLogHi | fLogLo);
#elif VECLEN == 2
   iLog = (vlong)_mm_shuffle_epi32((__m128i)iLog, 0x8);
   fLog = (vlong)_mm_shuffle_epi32((__m128i)fLog, 0x8);

   vdouble result = _mm_cvtepi32_pd((__m128i)iLog) +
      FIXED_SCALE*_mm_cvtepi32_pd((__m128i)fLog);
#endif

   // Convert from base 2 logarithm and extract result.
   return LOG2ERECIP*result;
}

// Original computation.
double fallback(double u1, double u2) {
   double w1 = M_PI*(u1-1/2.0);
   double w2 = -log(u2);
   return tan(w1)*(M_PI_2-w1)+log(w2*cos(w1)/(M_PI_2-w1));
}

int main() {
   static const vdouble ZERO = VBROADCAST(0.0)
   static const vdouble ONE = VBROADCAST(1.0);
   static const vdouble ONE_HALF = VBROADCAST(0.5);
   static const vdouble PI = VBROADCAST(M_PI);
   static const vdouble PI_2 = VBROADCAST(M_PI_2);

   int i,j;
   vdouble x = ZERO;
   for(i = 0; i < 100000000; i += VECLEN) {
      vdouble u1, u2;
      for (j = 0; j < VECLEN; ++j) {
         ((double *)&u1)[j] = mt_drand();
         ((double *)&u2)[j] = mt_drand();
      }

      vdouble w1 = PI*(u1 - ONE_HALF);
      vdouble w2 = -sse_log(u2);

      vdouble sin_w1 = sse_sin(w1);
      vdouble sin2_w1 = sin_w1*sin_w1;

#if VECLEN == 4
      int nearOne = _mm256_movemask_pd(sin2_w1 >= FALLBACK_THRESHOLD);
#elif VECLEN == 2
      int nearOne = _mm_movemask_pd(sin2_w1 >= FALLBACK_THRESHOLD);
#endif
      if (!nearOne) {
#if VECLEN == 4
         vdouble cos_w1 = _mm256_sqrt_pd(ONE - sin2_w1);
#elif VECLEN == 2
         vdouble cos_w1 = _mm_sqrt_pd(ONE - sin2_w1);
#endif
         vdouble tan_w1 = sin_w1/cos_w1;

         x += tan_w1*(PI_2 - w1) + sse_log(w2*cos_w1/(PI_2 - w1));
      }
      else {
         vdouble result;
         for (j = 0; j < VECLEN; ++j)
            ((double *)&result)[j] = fallback(((double *)&u1)[j], ((double *)&u2)[j]);
         x += result;
      }
   }

   double sum = 0.0;
   for (i = 0; i < VECLEN; ++i)
      sum += ((double *)&x)[i];

   printf("%lf\n", sum);
   return 0;
}

я столкнулся с одной проблемой раздражает -sin ошибка аппроксимации около ±pi / 2 может поставить значение немного вне [-1, 1] и что (1) вызывает вычисление tan быть недопустимым и (2) вызывает эффекты outsized, когда аргумент log близок к 0. Чтобы избежать этого, код проверяет if sin(w1)^2 "близко" к 1, и если это так, то он возвращается к исходному полному двойной точности путь. Определение "закрыть" находится в FALLBACK_THRESHOLD в верхней части программы-я произвольно установил его в 0.999, который по-прежнему возвращает значения в диапазоне исходной программы OP, но мало влияет на производительность.

я отредактировал код, чтобы использовать расширения синтаксиса GCC-specific для SIMD. Если у вашего компилятора нет этих расширений, вы можете вернуться к истории редактирования. Теперь код использует AVX, если он включен в компиляторе для процесс 4 удваивается одновременно (вместо 2 удваивается с SSE2).

результаты на моей машине без вызова mt_seed() чтобы получить повторяемый результат:

Version   Time         Result
original  14.653 secs  -1917488837.945067
SSE        7.380 secs  -1917488837.396841
AVX        6.271 secs  -1917488837.422882

это имеет смысл, что результаты ГСП/с AVX отличаются от оригинального результата trancendental приближений. Я думаю, вы должны быть в состоянии щипнуть FALLBACK_THRESHOLD обменять точность и скорость. Я не уверен, почему результаты SSE и AVX немного отличаются друг от друга.


можно переписать

tan(w1)*(M_PI_2-w1)+log(w2*cos(w1)/(M_PI_2-w1))

as

tan(w1)*(M_PI_2-w1) + log(cos(w1)/(M_PI_2-w1)) + log(w2).

вы, вероятно, можете ездить с минимаксными полиномами для материала в зависимости от w1 здесь. Составьте 64 из них или около того, каждый для 1/64-го диапазона, и вам, вероятно, нужна только степень 3 или 4.

вы вычисляете w2 as

w2 = -log(u2);

единого u2 на (0,1). Итак, вы действительно вычисляете log(log(1/u2)). Бьюсь об заклад, вы можете использовать аналогичный трюк, чтобы получить кусочно-полиномиальный приближения к log(log(1/x)) на куски (0,1). (Функция действует страшно рядом 0 и 1, поэтому вам, возможно, придется сделать что-то необычное там.)


Мне нравится предложение @tmyklebu о создании минимаксного приближения к общему расчету. Есть некоторые хорошие инструменты, чтобы помочь с этим, в том числе инструментарий аппроксимации функции Ремеза

вы можете сделать намного лучше, чем MT для скорости; см., например, это статья доктора Доббса: быстрые, высококачественные, параллельные генераторы случайных чисел

также взгляните на ACML-AMD Core Math Library воспользоваться SSE и Поддержкой SSE2.


во-первых, небольшое преобразование. Это первоначальная сумма:

for(i = 0; i < 100000000; ++i) {
    u1 = mt_drand();
    u2 = mt_drand();
    w1 = M_PI*(u1-1/2.0);
    w2 = -log(u2);
    x += tan(w1)*(M_PI_2-w1)+log(w2*cos(w1)/(M_PI_2-w1));
}

эта сумма математически эквивалентна:

for(i = 0; i < 100000000; ++i) {
    u1 = M_PI - mt_drand()* M_PI;
    u2 = mt_drand();
    x += u1 / tan (u1) + log (sin (u1) / u1) + log (- log (u2));
}

и поскольку он должен быть эквивалентен замене mt_drand () на 1.0 - mt_rand (), мы можем позволить u1 = mt_drand () * M_PI.

for(i = 0; i < 100000000; ++i) {
    u1 = mt_drand()* M_PI;
    u2 = mt_drand();
    x += u1 / tan (u1) + log (sin (u1) / u1) + log (- log (u2));
}

Итак, теперь это красиво разделено на две функции случайной величины, которые можно обрабатывать отдельно; x += f (u1) + g (u2). Обе функции славно ровны над долгосрочными диапазонами. f вполне гладко для say u1 > 0.03, а 1 / f довольно гладко для меньших значений. g довольно гладкий, за исключением значений, близких к 0 или 1. Так что мы могли бы использовать, скажем, 100 различных приближений для интервалов [0 .. 0.01], [0.01 .. 0.02] и так далее. За исключением того, что выбор правильного приближения занимает много времени.

для решения этой задачи: линейная случайная функция в интервале [0 .. 1] будет иметь определенное количество значений в интервале [0 .. 0.01], другое число значений в [0.01 .. 0.02] и так на. Я думаю, вы можете подсчитать, сколько случайных чисел из 100,000,000 попадают в интервал [0 .. 0.01], предполагая нормальное распределение. Затем подсчитать, сколько из оставшихся попадают в [0.01 .. 0.02] и так далее. Если вы вычислили, что, скажем, 999,123 числа попадают в [0.00, 0.01], то вы производите это число случайных чисел в интервале и используете одно и то же приближение для всех чисел в интервале.

найти аппроксимацию f (x) в интервале [0.33 .. 0.34], как пример, вы приближаете f (0.335 + x / 200) для x в [-1 .. 1]. Вы получите достаточно хорошие результаты, взяв интерполирующий полином степени n, интерполирующий в узлах Чебысева xk = cos (pi * (2k - 1) / 2n).

кстати, выполнение старых x87 тригонометрических и логарифмических операций-это медленно. Абсолютно нигде рядом с оценкой полинома низкой степени. И с достаточно небольшими интервалами вам не нужен высокий полином степень.


  1. Как c вычисляет sin () и другие математические функции?
  2. Не совсем оправдано. Таблица для 32 бит точности (что означает, что вы хотите, чтобы математика с фиксированной точкой не удваивалась, но я отвлекусь, должна быть (2^32)*4 байта. Вы можете уменьшить это, если ваши "32 бита точности" - это выход, а не вход (он же диапазон ввода от 0 до 2PI представлен в

как вы сказали, некоторые трансцендентальные функции, такие как синус, Косинус и касательной доступны в качестве инструкций по сборке в архитектуре x86. Вероятно, именно так библиотека C реализует sin(), cos(), tan() и друзей.

однако некоторое время назад я немного повозился с этими инструкциями, переопределив функции как макросы и удалив каждую проверку ошибок и проверку, чтобы оставить только минимум. Тестирование против библиотеки C, я помню свои макрофункции, где довольно быстро. Вот пример моей пользовательской функции касательной (простите синтаксис сборки Visual Studio):

#define machine_tan_d(result, x)\
__asm {\
    fld qword ptr [x]\
    fptan\
    fstp st(0)\
    fstp qword ptr [result]\
}

поэтому, если вы готовы сделать некоторые предположения, удалить обработку ошибок / проверку и сделать вашу платформу кода конкретной, то вы можете сжать несколько циклов, используя макрофункцию, как я сделал.

теперь о второй теме, используя таблицу поиска, я бы не был так уверен это быстрее только потому, что вы будете использовать целочисленные операции. Таблица целых чисел помещает дополнительные накладные расходы в кэш данных, что может привести к более частым пропускам кэша и худшему времени выполнения, чем операции float. Но это, конечно, можно сделать только путем тщательного профилирования и бенчмаркинга.


процессор, вероятно, реализует tan() и cos () как собственные инструкции (проводной или микрокод)FPTAN (x87, так+) и FCOS (387+) для x86/87 (87 из оригинального математического сопроцессора Intel 8087).

поэтому в идеале ваша среда должна генерировать и выполнять собственные инструкции x87, а именно FCOS и FPTAN (частичный загар). Сгенерированный код сборки можно сохранить с помощью -S флаг с gcc для явного создания вывода и поиска на языке ассемблера за эти инструкции. Если нет, проверьте использование флагов, включающих генерацию для правильного процессора подмодель (или шкаф доступный) для gcc.

Я не верю, что есть какие-либо наборы инструкций SIMD (MMX, SSE, 3dNow и т. д.), которые обрабатывают такие функции log(), tan(), cos (), так что это не (прямой) вариант, но инструкции SIMD отлично подходят для интерполяции из ранее вычисленных результатов или из таблицы.

другой такт был бы попробовать некоторые параметры математической оптимизации, доступные в компиляторе GCC. Такие как -ffast-math что может быть опасно, если вы не понимаете последствий. Вариант округления может быть достаточным, если проблема скорости просто связана с разницей между собственной 80-битной расширенной точностью x87 и 64-битным стандартом IEEE 754 double точной цифры.

Я не ожидаю, что вы можете легко написать приближение, которое подходит и правильно для 32-битного плавающего или фиксированный номер точки и сделать это быстрее чем родные инструкции FPU. Неясно, насколько точно вам нужно / хотите следовать определенной кривой распределения, как и в большинстве вещей, связанных с PRNG, дьявол находится в мельчайших деталях.

гарантируя, что вы, по крайней мере, используете собственные инструкции по сборке с плавающей запятой для basic элементарно (трансцендентальные) математические функции-хорошая отправная точка, возможно, лучшее улучшение производительности чтобы использовать математические упрощения, такие как tmyklebu и gnasher729 в своих ответах.

далее, создание аппроксимации функции неравномерного распределения, как это предлагается @tmyklebu в их ответ, и другие, создания минимаксные аппроксимации С помощью Алгоритм Ремеза этой функции распределения было бы лучшим подходом. Это лучше, чем творить аппроксимации отдельных элементарных математических функций (log,cos и т. д.) создать один полиномиальная аппроксимация всей функции отображения распределения.

помимо этого я бы порекомендовал две книги современные методы и алгоритмы с плавающей запятой,элементарные функции, алгоритмы и реализация, 2-е изд. и Справочник по арифметике с плавающей запятой оба Жан-Мишель Мюллер (редактор второго названия). Первый в большей степени ориентирован на осуществление, тогда как второй является весьма всеобъемлющим, но все же легко понятным.

С любой из этих книг вы должны быть в состоянии понять точность и скорость компромиссов для вас ситуации и написать достаточную реализацию.

лично мне не рекомендуется использовать компьютерные приближения Харта (1968 или 1978 репринта) он просто слишком устарел и слишком удален из современного компьютерного оборудования рекомендовать, но легко найти используемую или библиотечную копию или математический инструментарий Джека Креншоу для программирования в реальном времени, который действительно ориентирован на неточные встроенные приложения.

Jack Ganssle имеет две части, вводящие приближение для встроенных приложений,приближения для корней и экспонент и руководство по приближениям (PDF). Хотя я абсолютно не рекомендую приведенные формулы для 32 (+)-битных процессоров, особенно если у них есть FPU, они являются мягким введением в основы.


1) "Это зависит"... Зависит от компилятора больше, чем от архитектуры чипа. 2) в былые времена было популярно пользоваться методами CORDIC для реализации функций тригонометрии. http://en.wikipedia.org/wiki/CORDIC