【问题标题】:FAST get 10 to the power n(10^n) in C/C++在 C/C++ 中快速获得 10 的 n(10^n) 次幂
【发布时间】:2021-11-12 10:41:47
【问题描述】:

我想快速计算 10(是的,只有 10)的 n[0..308] 次幂。我想出了一些方法。

1)

double f(int n) {
  return pow(10.0, n);
}
double f1(int n) {
  double a = 10.0;
  double res = 1.0;
  while(n) {
    if(n&1) res *= a;
    a *= a;
    n >>= 1;
  }
  return res;
}

时间:O(logn),可以更快吗? ( // f1() 可以做一点优化,但仍然是 O(logn))

2)

double f2(int n) {
  static const double e[] = { 1e+0, 1e+1, 1e+2, ..., 1e+308 };
  return e[n];
}

时间:O(1),非常好。 但是空间:309 * 8 字节 = 2472 字节……哎呀,它太大了……

3)

double f3(int n){
    static const double e[] = {
        1e+1, 1e+2, 1e+4, 1e+8, 1e+16, 1e+32, 1e+64, 1e+128, 1e+256
    };
    double res = 1.0;
    for(int i = 0; n; ++i){
        if(n & 1){
            res *= e[i];
        }
        n >>= 1;
    }
    return res;
}

f3 结合 f1 和 f2 以避免乘法,例如 1e128*1e128,我希望它更快,但是.. 实际上 f3 比 f2 慢.. 因为 ++i 我猜..

好吧,在我输入这些代码之前我几乎放弃了,

int main(){
    double d = 1e+2;
    return 0;
}

并通过g++编译成.s

LCPI0_0:
    .quad   0x4059000000000000              ## double 100

编译器如何知道 1e+2 是 0x4059000000000000?

我的意思是我想要得到的只是一个双值 1e+n。但是当编译器编译“double d = 1e+2”时,它知道 d 应该是 0x4059000000000000。我可以使用某种方法直接返回 1e+n 之类的东西吗?或者我可以做一些 C/C++ 之外的事情来获得我的价值吗?

非常感谢。如有错误或不清楚的地方请指出。

【问题讨论】:

  • 您的问题是编译器如何知道如何存储1e+2?也许this 会对此有所了解。
  • 1e+2 是一个双 literal - 如果编译器无法将文字翻译成其 IEEE 754 表示(或任何可能的表示),它怎么能完成它的工作?在混凝土机器上使用,IEEE 754 不是强制性的......)。
  • 如果这对性能至关重要并且没有嵌入到一些微小的硬件上,那么 f2 中的表格似乎并不大。请注意,您还可以有一个较小的表,例如最多 32 个,将 n 拆分为 a+b
  • 你经常这样做吗?如果是,那么 SIMD 将是更好的解决方案

标签: c++ compiler-construction double pow


【解决方案1】:

编译器如何知道 1e+2 是 0x4059000000000000?

因为1e+2 是一个文字(所以是一个编译时常量)并且编译器知道目标架构。因此它可以直接将常量存储在目标程序中。请注意,如果它可以在编译时推导出 n 的值并启用优化,它可以计算像 pow(10.0, n) 这样的常量。因为,n 很可能是一个变量,所以需要在运行时 计算 10e+n(例如,使用 pow(10.0, n),就像你所做的那样)。如果您知道输入 n 始终是编译时常量,那么您可以使用 constexpr(或模板)。

f3 结合 f1 和 f2 以避免乘法,例如 1e128*1e128,我希望它更快,但是.. 实际上 f3 比 f2 慢.. 因为 ++i 我猜..

不,它有点复杂。 f3 不是很快,因为循环携带的依赖性会阻止您的处理器有效地执行循环。事实上,现代处理器是superscalar,只要指令之间没有依赖关系,它们就可以并行执行许多循环迭代。在这种情况下,瓶颈来自于每次迭代中对res 的(完全顺序)修改以及相关浮点指令的高延迟。

请注意,条件和循环的可预测性也起着重要作用。

我想快速计算 10(是的只有 10)的 n[0..308] 次幂

f2 如果表已经存储在 L1 缓存中(因此可以放入其中),则可以非常快。现在几乎所有主流的现代处理器都至少有 16 KiB 的缓存。 f2 可能比f3 慢,如果表存储在 RAM 中并且需要检索到缓存(由于 RAM 的高延迟导致非常慢的缓存未命中)。

您可以使用编译器轻松展开的手动归约大大加快f3的计算速度。下面是代码示例:

double f4(uint32_t n) {
    static const double e[] = {
        1e+1, 1e+2, 1e+4, 1e+8, 1e+16, 1e+32, 1e+64, 1e+128, 1e+256
    };

    double p[9];

    for(int i=0 ; i<9 ; ++i)
        p[i] = (n & (1 << i)) ? e[i] : 1.0;

    return ((p[0] * p[4]) * (p[1] * p[5])) * ((p[2] * p[6]) * (p[3] * p[7])) * p[8];
}

编译器 Clang 为 f4 生成相对较好的指令,从而加快执行速度。不幸的是,像 GCC 这样的一些编译器为f4 生成了一个非常糟糕的代码:它们使用缓慢的条件跳转。不过,这种基于缩减的方法远非便宜。

正如@MarcGlisse 在 cmets 中提出的那样,您可以在非常小的表上进行 2 次查找,以便快速实现:

double f6(uint32_t n) {
    static const double eLow[] = { 1e+0, 1e+1, 1e+2, 1e+3, 1e+4, 1e+5, 1e+6, 1e+7, 1e+8, 1e+9, 1e+10, 1e+11, 1e+12, 1e+13, 1e+14, 1e+15, 1e+16, 1e+17, 1e+18, 1e+19, 1e+20, 1e+21, 1e+22, 1e+23, 1e+24, 1e+25, 1e+26, 1e+27, 1e+28, 1e+29, 1e+30, 1e+31 };
    static const double eHigh[] = { 1e+0, 1e+32, 1e+64, 1e+96, 1e+128, 1e+160, 1e+192, 1e+224, 1e+256, 1e+288 };
    return eLow[n & 0x1F] * eHigh[n >> 5];
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-09-14
    • 2018-10-08
    • 2011-09-03
    • 2016-04-27
    • 1970-01-01
    • 1970-01-01
    • 2022-06-17
    • 2023-03-14
    相关资源
    最近更新 更多