提高计算1到2之间数字的log2的性能

时间:2017-07-29 14:41:01

标签: c++ c logarithm fixed-point

我正在尝试使用整数运算来计算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;
}

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

任何想法都将受到高度赞赏。

3 个答案:

答案 0 :(得分:2)

虽然你在评论中说你不想要一个基于查找表的解决方案,但我仍然在这里提出一个。原因很简单:这个查找表是516个字节。如果我用Log2编译你的-O3,我会得到一个~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()
  • 减小表格大小会进一步显着增加最差错误

答案 1 :(得分:0)

由于您的代码已经非常快,我会尝试展开循环。编写循环体16次会使代码无法读取,但会节省循环开销,表达式1 << (15 - i)将变为常量。

答案 2 :(得分:0)

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

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

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

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

所以几乎没有分支,没有缓存刷新和&amp;错过(或至少是最小数量) - 简单的流水线,最佳执行时间