【问题标题】:How to speed up tricky random number generation如何加快棘手的随机数生成
【发布时间】:2014-07-13 03:06:52
【问题描述】:

我有一些代码可以对双打执行许多日志、tancos 操作。我需要尽可能快。目前我使用的代码如

#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("%f\n",x); 

   return EXIT_SUCCESS;
}

我正在使用 gcc。

有两种明显的方法可以加快速度。首先是选择更快的RNG。二是加速超越函数。
为此,我想知道

  1. 如何在 x86 上的汇编中实现 tan 和 cos?我的 CPU 是 AMD FX-8350,如果有区别的话。 (对于cos,请回答fcos,对于tan,请回答fptan。)
  2. 如何使用查找表来加快计算速度?我只需要 32 位的精度。例如,您可以使用 2^16 大小的表格来加快 tan 和 cos 运算吗?

Intel optimization manual

如果没有关键需要评估超越函数 使用 80 位的扩展精度,应用程序应考虑 另一种基于软件的方法,例如基于查找表的方法 使用插值技术的算法。有可能改进 通过选择这些技术的超凡性能 所需的数值精度和查找表的大小,并通过 利用 SSE 和 SSE2 的并行性 说明。

根据这个很有帮助的tablefcos 的延迟为 154,fptan 的延迟为 166-231


您可以使用

编译我的代码

gcc -O3 -Wall random.c mtwist-1.5/mtwist.c -lm -o random

我的 C 代码使用来自 here 的 Mersenne Twister RNG C 代码。你应该能够运行我的代码来测试它。如果不能,请告诉我。


更新 @rhashimoto 已将我的代码从 20 秒加快到 6 秒!

RNG 似乎应该可以加速。但是在我的测试中http://www.math.sci.hiroshima-u.ac.jp/~%20m-mat/MT/SFMT/index.html#dSFMT 花费的时间完全相同(有没有人看到任何不同的东西)。如果有人能找到更快的 RNG(通过所有顽固测试),我将不胜感激。

请显示您建议的任何改进的实际时间安排,因为这确实有助于确定哪些有效或无效。

【问题讨论】:

  • 您是否真的分析了您的代码并得出了瓶颈是这些功能的结论,或者您只是进入了一个未成熟的优化?
  • @PeterVaro 该代码从一个棘手的分布中产生随机数。因此,除了从 (0,1) 中选择一个随机数并应用 tan 和 cos(实际上是 log)之外,它几乎没有做任何事情。我需要优化这两个部分。我只需要数十亿个。
  • @user2179021 我发现,在描述此类系统时,进行数学运算通常比使用蒙特卡洛法更准确、更快速。
  • 如果不检查详细信息:每个 tan、cos、log 的基本 斜率的 2^16 表可能会起作用。可能会提供 24 位以上的准确度。
  • 尝试 gcc 标志 -march=native -mtune=native 以优化您的构建机器。此外,您不需要从头开始计算 costan - 尝试 sincossqrt(1.0 - cosw1*cosw1)/cosw1(后者对我来说更快)。

标签: c performance math assembly micro-optimization


【解决方案1】:

你可以重写

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

作为

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

您可能可以根据w1 这里的东西使用极小多项式。组成 64 个左右,每个占范围的 1/64,您可能只需要 3 或 4 级。

您将w2 计算为

w2 = -log(u2);

对于(0,1) 中的统一u2。所以你真的在计算log(log(1/u2))。我敢打赌,您可以使用类似的技巧在 (0,1) 的块上获得对 log(log(1/x)) 的分段多项式逼近。 (这个函数在01 附近表现得很吓人,所以你可能需要在那里做一些花哨的事情。)

【讨论】:

  • 谢谢。我很想听到更多关于这个的信息。事实上,我的代码中有一个错误,因为我不希望 u1 或 u2 等于 0。
  • 由于您可以将u2 生成为random_i2/random_period,因此您的-log(u2) 可以计算为-log(random_i2) + log(random_period),并且random_period 是常数。
【解决方案2】:

我喜欢@tmyklebu 的建议,即为整体计算创建一个极小极大近似值。有一些不错的工具可以帮助解决这个问题,包括 Remez function approximation toolkit

在速度方面,您可以比 MT 做得更好;例如看这个Dr. Dobbs article: Fast, High-Quality, Parallel Random Number Generators

还可以查看ACML – AMD Core Math Library 以利用 SSE 和 SSE2。

【讨论】:

  • 谢谢。有人告诉我,为了获得最终速度,您需要直接创建浮点数,而不是先创建整数然后除。但我不知道该怎么做。
  • 整数随机数到浮点数的转换见doornik.com/research/randomdouble.pdf
  • Sollya 也是一回事。
  • @tmyklebu,我能够很容易地在 OSX 上构建 Sollya,(除了 libfplll 之外的所有依赖项都在 macports 中),它看起来非常好。谢谢。
  • @user2179021:我没有尝试过使用您的特定功能,但是将类似的技术应用于log(1 + exp(x))1 / (1 + exp(-x))(具有适当的低精度和较小的范围)可以加快一些实现逻辑回归分类器。你的功能有点糟糕。
【解决方案3】:

您可以尝试我使用 SSE2 内在函数编写的 log(x) 替换:

#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]
}

该代码实现了this Texas Instruments brief 中描述的算法,该算法反复平方参数并连接指数位。它适用于非规范化的输入。它提供至少 30 位的精度。

这在我的一台机器上运行速度比log() 快,而在另一台机器上运行速度慢,因此您的里程可能会有所不同;我并不认为这一定是最好的方法。但是,此代码实际上使用 128 位 SSE2 字的两半并行计算两个对数(尽管函数原样只返回一个结果),因此它可以适应整个 SIMD 计算的一个构建块功能(我认为log 是困难的部分,因为cos 表现得非常好)。此外,您的处理器支持 AVX,它可以将 4 个双精度元素打包成一个 256 位字,并且将此代码扩展到 AVX 应该很简单。

如果您选择不使用完整的 SIMD,您仍然可以通过流水线使用两个对数槽 - 即计算 log(w2*cos(w1)/(M_PI_2-w1)) 用于当前迭代,log(u2) 用于 下一个 迭代。

即使这个函数单独对log 进行基准测试较慢,它仍然值得用您的实际函数进行测试。此代码根本不强调数据缓存,因此它可能与其他代码更友好(例如,使用查找表的cos)。微指令调度也可以通过周围的代码改进(或不改进),这取决于其他代码是否使用 SSE。

我的其他建议(从 cmets 重复)是:

  • 尝试-march=native -mtune=native 获取 gcc 以针对您的 CPU 进行优化。
  • 避免在同一个参数上同时调用 tancos - 使用 sincos 或三角标识。
  • 考虑使用 GPU(例如 OpenCL)。

似乎计算sin 而不是cos 更好——原因是您可以将它用于tan_w1 = sin_w1/sqrt(1.0 - sin_w1*sin_w1)。使用我最初建议的cos 在计算tan 时会丢失正确的符号。正如其他回答者所说,您可以通过在 [-pi/2, pi/2] 上使用极小多项式来获得很好的加速。 this link 的 7-term 函数(确保你得到 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;
}

我遇到了一个恼人的问题 - ±pi/2 附近的 sin 近似误差可以使值稍微超出 [-1, 1] 并且 (1) 导致 tan 的计算无效并且 ( 2) 当 log 参数接近 0 时会导致过大的影响。为了避免这种情况,代码测试 sin(w1)^2 是否“接近”到 1,如果是,则返回到原始的完整双精度路径。 “关闭”的定义在程序顶部的 FALLBACK_THRESHOLD 中 - 我随意将其设置为 0.999,它似乎仍然返回 OP 原始程序范围内的值,但对性能影响不大。

我已编辑代码以将 gcc-specific syntax extensions 用于 SIMD。如果您的编译器没有这些扩展,那么您可以返回编辑历史记录。如果在编译器中启用,代码现在使用 AVX 来一次处理 4 个双精度数(而不是使用 SSE2 的 2 个双精度数)。

在我的机器上没有调用mt_seed() 以获得可重复结果的结果是:

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

由于超越近似,SSE/AVX 结果与原始结果不同是有道理的。我认为您应该能够调整FALLBACK_THRESHOLD 以权衡精度和速度。我不确定为什么 SSE 和 AVX 结果会略有不同。

【讨论】:

  • 将 tan(w1) 替换为 tanw1 = sqrt(1.0 - cosw1*cosw1)/cosw1;将时间缩短 5 秒(从 20 秒到 15 秒)。使用 -march=native -mtune=native 几乎没有区别。我现在就试试你的日志。我喜欢使用 AVX 的解决方案,但我不认为我有能力独自完成。
  • 您介意添加完整代码或告诉我如何修复paste.ubuntu.com/7560751吗?
  • 我得到错误:初始化元素不是常量 static const __m128d FIXED_SCALE = _mm_set1_pd(9.31322574615478515625e-10); // 2^-30
  • 这是一个 C 错误; C++ 允许您使用函数调用来初始化静态数据。我修改了我的答案以使用 C 构建,但现在您必须在初始化程序时调用 sselog_init()。我还添加了#include &lt;assert.h&gt; 并使用gcc -o a.out -O3 -march=native -mtune=native -DNDEBUG -Imtwist-1.5 test.c mtwist-1.5/mtwist.c -lm 成功构建。
  • 现在缩短到 12 秒!奇怪的是 -march=native -mtune=native 实际上减慢了 0.3 秒。
【解决方案4】:

首先,进行一点改造。这是原始总和:

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 对于 u1 > 0.03 非常平滑,而 1 / f 对于较小的值非常平滑。 g 非常平滑,除了接近 0 或 1 的值。所以我们可以对区间 [0 .. 0.01]、[0.01 .. 0.02] 等使用 100 种不同的近似值。除了选择正确的近似值非常耗时。

解决这个问题:区间 [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],那么您会在区间中生成该数量的随机数,并对区间中的所有数字使用相同的近似值。

要在区间 [0.33 .. 0.34] 中找到 f (x) 的近似值,例如,您在 [-1 .. 1] 中为 x 近似 f (0.335 + x / 200)。通过采用 n 次插值多项式,在 Chebysev 节点 xk = cos (pi * (2k - 1) / 2n) 处插值,您将获得相当好的结果。

顺便说一下,旧的 x87 三角和对数运算的性能很慢。绝对远不及评估低次多项式。并且间隔足够小,您不需要高多项式次数。

【讨论】:

  • 谢谢你。要获得 32 位精度,您最后的建议需要什么尺寸的表?
【解决方案5】:

处理器可能将 tan() 和 cos() 实现为本机指令(硬连线或微码)FPTAN (x87+) 和 x86/87 的 FCOS (387+)(87 来自原始数学协处理器,英特尔 8087)。

因此,理想情况下,您的环境应该生成并执行本机 x87 指令,即 FCOSFPTAN(部分棕褐色)。您可以使用-S 标志和gcc 保存生成的汇编代码,以显式生成汇编语言输出并搜索这些指令。如果不是,请验证标志的使用情况,以便为 gcc 生成正确的处理器 submodel(或可用的壁橱)。

我不相信有任何 SIMD 指令集(MMX、SSE、3dNow 等)可以处理 log()、tan()、cos() 等函数,因此这不是(直接)选项,但 SIMD 指令非常适合从先前计算的结果或表中进行插值。

另一种方法是尝试 GCC 编译器提供的一些数学优化选项。例如-ffast-math,如果您不了解其中的含义,可能会很危险。如果速度问题仅与 x87 的本机 80 位扩展精度和 64 位 IEEE 754 标准 double 精度数之间的差异有关,则舍入选项可能就足够了。

我不希望您可以轻松地编写适合和正确的 32 位浮点数或定点数的近似值,并使其比原生 FPU 指令更快。目前尚不清楚您需要/想要多准确地遵循特定的分布曲线,就像与 PRNG 相关的大多数事情一样,魔鬼在微小的细节中。

虽然确保您至少对基本 elementary(超越)数学函数使用本机汇编浮点指令是一个很好的起点,但最好的性能改进可能是利用数学简化,例如 tmyklebugnasher729 在他们的回答中。

接下来,按照以下建议创建非均匀分布函数的近似值 @tmyklebu 在他们的answer 和其他人中,使用此分布函数的Remez Algorithm 创建minimax approximation 将是最好的方法。这不是创建单个基本数学函数(log、cos 等)的近似值,而是创建整个分布映射函数的单个多项式近似值。

除此之外,我还推荐两本关于现代浮点方法和算法的书籍,Elementary Functions, Algorithms and Implementation, 2nd ed.Handbook of Floating-Point Arithmetic 均由 Jean-Michel Muller(第二本书的编辑)撰写。第一个更面向实现,而第二个非常全面但仍然易于理解。

通过这两本书中的任何一本,您都应该能够了解针对您的情况在精度与速度之间进行权衡并编写足够的实现。

就个人而言,我确实推荐使用 Hart 的计算机近似(1968 年,或 1978 年的 重印),它实在是太陈旧了,而且从现代计算机硬件中脱离了太多,无法推荐,但是很容易找到使用过的或库副本,或者 Jack Crenshaw 的实时编程数学工具包,它真正面向非精度嵌入式应用程序。

Jack Ganssle 有两篇文章介绍了嵌入式应用程序的近似值,Approximations for Roots and ExponentialsA Guide to Approximations (PDF)。虽然我绝对不推荐 32(+) 位处理器的给定公式,特别是如果它们有 FPU,它们只是对基础知识的温和介绍。

【讨论】:

    【解决方案6】:
    1. How does C compute sin() and other math functions?
    2. 不太可行。一个 32 位精度的表(这意味着您希望定点数学不加倍,但我离题的长度必须是 (2^32)*4 字节。如果您的“32 位精度”,您可以缩小一些" 是输出而不是输入(也就是 0 到 2PI 的输入范围以

    【讨论】:

    • 您不需要包含 2^32 个单词的完整表格。使用插值甚至 CORDIC 等技术需要小得多的表。
    • @markgz 是的,但是一旦你开始介绍这些技术,“只有 32 位准确”的问题就会出现(32 位准确度太高了,没有必要)。
    • 实际上,在实现三角函数时,您通常会缩小范围,降至 \{ 0 \dots \frac{\pi}{2} \} 或 \{ \frac{-\Pi}{4} \dots \frac{\pi}{4} \}
    • 我应该说 0 .. Pi/4,作为单一的减少目标。可以根据输入在单位圆(正方形)上的位置通过简单的变换来调整结果,以找到正确的答案。
    • @MadScienceDreams:使用表格获得 32 个正确位真的不难。
    【解决方案7】:

    正如您所说,sinecosinetangent 等一些超越函数在 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]\
    }
    

    因此,如果您愿意做出一些假设,移除错误处理/验证并使您的代码特定于平台,那么您可以像我一样使用宏函数来压缩几个周期。

    现在关于第二个主题,使用查找表,我不确定它是否会更快,因为您将使用整数运算。整数表会在数据缓存中增加额外的开销,可能导致比浮点操作更频繁的缓存未命中和最差的执行时间。 但这当然只能通过仔细的分析和基准测试来推断。

    【讨论】:

    • 为什么我们应该在 sse2 时代使用 x87,以及何时使用(64 位)双精度?
    • 同意。我是在 2009 年左右写的,但是,它比任何现实生活中的用途更值得好奇。今天编写汇编代码并没有太多好处,编译器内在函数和其他不可用的东西。
    • @osgx 我认为真正的加速将来自使用 AVX.. 但首先我需要找到知道如何为 AVX 编码的人!
    【解决方案8】:

    1)“这取决于”......更多地取决于编译器而不是芯片的架构。 2)在过去,使用 CORDIC 方法来实现三角函数很流行。 http://en.wikipedia.org/wiki/CORDIC

    【讨论】:

    • 本例编译器为gcc。
    • 我不认为,CORDIC 不再具有竞争力。维基百科文章引用了它的主要优势,即它不必进行乘法运算。但是使用现代硬件进行乘法运算非常便宜。如果我没记错的话,你需要对 CORDIC 进行大量迭代才能获得不错的精度。
    • @cmaster 对。这个问题的速度问题是cos、log和我认为的RNG。 cos好像特别慢。
    猜你喜欢
    • 2017-01-08
    • 2010-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-27
    • 2023-03-22
    • 2017-09-23
    • 2011-02-19
    相关资源
    最近更新 更多