【问题标题】:Improve performance of calculating log2 of a number between 1 and 2 [closed]提高计算 1 到 2 之间数字的 log2 的性能 [关闭]
【发布时间】:2017-07-29 14:41:01
【问题描述】:

我正在尝试使用整数算术运算来计算 log2(x)

输入 x 是一个介于 1 和 2 之间的值。

因为这只会产生 0,所以一切都预先缩放 16。

换句话说:

  • 该函数采用整数值x * 2^16 而不是x
  • 函数返回log2(x) * 2^16的整数值而不是log2(x)

这是我的代码:

uint64_t Log2(uint64_t x)
{
    static uint64_t TWO = (uint64_t)2 << 16;

    uint64_t res = 0;

    for (int i=0; i<16; i++)
    {
        x = (x * x) >> 16;
        if (x >= TWO)
        {
            x >>= 1;
            res += 1 << (15 - i);
        }
    }

    return res;
}

我正在寻找的是一种提高循环性能的方法。

【问题讨论】:

  • 让循环向后运行?那么就不需要31 - i了。
  • 你的帖子更适合Code Review
  • 因此是一个深度流水线、无序架构,具有快速、完全流水线的乘法。然后,我建议用无分支代码替换 if。我也会仔细研究尝试减少依赖链的长度,但这可能对这个算法来说很难——每次迭代都依赖于上一次迭代中x 的值,极大地限制了 OoOE 引擎利用指令级并行性。
  • @Olaf:你真的认为,在优化方面,AVR、x86 和 GPU 完全一样吗?
  • 功能代码的审查与 Stack Overflow 无关。此类问题属于Code Review

标签: c++ c logarithm fixed-point


【解决方案1】:

虽然您在 cmets 中说您不想要基于查找表的解决方案,但我仍然在这里提供一个。原因很简单:这个查找表是 516 字节。如果我用-O3 编译你的Log2,我会得到一个~740 字节的函数,所以它在同一个范围内。

我没有创建与您的完美匹配的解决方案。原因很简单:您的版本没有尽可能精确。我使用rint(log(in/65536.0f)/log(2)*65536) 作为参考。您的版本产生的最差差异为 2,平均差异为 1.0。这个提议的版本的最差差为 1,平均差为 0.2。所以这个版本更准确。

关于性能:我检查了两个微基准:

  • 使用简单的 LCG 随机发生器作为输入。我的版本快 29
  • 线性使用数字 0x10000->0x20000。我的版本快 17

解决方案非常简单(使用initTable()初始化查找表),它在表格元素之间进行线性插值:

unsigned short table[0x102];
void initTable() {
    for (int i=0; i<0x102; i++) {
        int v = rint(log(i*0x100/65536.0f+1)/log(2)*65536);
        if (v>0xffff) v = 0xffff;
        table[i] = v;
    }
}

int log2(int val) {
    int idx = (val-0x10000)>>8;
    int l0 = table[idx];
    int l1 = table[idx+1];

    return l0+(((l1-l0)*(val&0xff)+128)>>8);
}

我刚刚玩过桌子,下面是进一步的结果:

  • 您可以将表大小减小到包含 0x82 个元素(260 字节),但仍然有 1 的最差误差和 0.32 的平均误差(在这种情况下,您需要将 0.5+ 放入 rint()
  • 您可以将表大小减小到包含 0x42 个元素(132 字节),最差错误变为 2,平均错误为 0.53(在这种情况下,您需要将 0.75+ 放入 rint()
  • 进一步减小表大小会显着增加最差错误

【讨论】:

    【解决方案2】:

    由于您的代码已经非常快,我会尝试展开循环。编写循环体 16 次会使代码不可读,但会节省循环开销,并且表达式 1 &lt;&lt; (15 - i) 变为常量。

    【讨论】:

      【解决方案3】:

      相信你的编译器:)。只需使用足够高的优化级别,编译器就会对这种微优化进行排序。

      例子: gcc ARM - https://godbolt.org/g/4XdPCp

      gcc - x86-64 https://godbolt.org/g/nBNmLR

      gcc - AVR https://godbolt.org/g/Mq81Sg

      因此几乎没有分支,没有缓存刷新和未命中(或至少很少) - 易于流水线化,最佳执行时间

      【讨论】:

      • 里面有很多分支,除了ARM版本
      • 与“正常”循环相比?你觉得够吗?而且很多都不会被执行
      • 主要与“所以没有分支机构”相比——那里有十几个分支机构,所以这是一个奇怪的说法。但可以肯定的是,即使与循环相比:它删除了可预测的分支,而不可预测的分支仍然存在。
      • OK 改成差不多了
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-11-15
      • 2011-08-06
      • 2010-09-06
      • 1970-01-01
      相关资源
      最近更新 更多