【问题标题】:performance of log10 function returning an int返回 int 的 log10 函数的性能
【发布时间】:2022-01-20 20:50:11
【问题描述】:

今天我需要一个便宜的 log10 函数,我只使用了 int 部分。假设结果是底,那么 999 的 log10 将是 2。我自己写一个函数会有好处吗?如果是这样,哪种方式是最好的选择。假设代码不会被优化。

我想到的 log10 的替代品;

  • 使用 for 循环除以或乘以 10;
  • 使用字符串解析器(可能非常昂贵);
  • 使用整数 log2() 函数乘以常数。

提前谢谢你:)

【问题讨论】:

  • 您需要一些快速但未优化的东西,没有特定的语言。祝你好运。
  • 你使用什么语言?
  • @Scott Hunter 抱歉,我没有更清楚地解释自己。我的意思是编译器优化。我最感兴趣的是返回整数的高效日志函数的概念。
  • 如果您要查找的只是整数日志,请查看:helloacm.com/fast-integer-log10rot47.net/share-picture/37。它们本质上是一样的。一个在 C# 中,一个在 C++ 中。
  • 嗯...答案似乎仅限于整数 argument - 但我在问题本身中看不到这个要求。例如,对于像 1.23e-12 这样的数字,要求 10 的幂的整数部分可能很有用......尤其是考虑到“或 乘以 10”作为选项之一在问题中考虑。

标签: c++ optimization integer logarithm


【解决方案1】:

该操作可以在(快速)恒定时间内在任何具有计数前导零或类似指令的架构(这是大多数架构)上完成。这是一个 C sn-p,我坐在那里计算以十为底的位数,这基本上是相同的任务(假设类似 gcc 的编译器和 32 位 int):

unsigned int baseTwoDigits(unsigned int x) {
    return x ? 32 - __builtin_clz(x) : 0;
}

static unsigned int baseTenDigits(unsigned int x) {
    static const unsigned char guess[33] = {
        0, 0, 0, 0, 1, 1, 1, 2, 2, 2,
        3, 3, 3, 3, 4, 4, 4, 5, 5, 5,
        6, 6, 6, 6, 7, 7, 7, 8, 8, 8,
        9, 9, 9
    };
    static const unsigned int tenToThe[] = {
        1, 10, 100, 1000, 10000, 100000, 
        1000000, 10000000, 100000000, 1000000000,
    };
    unsigned int digits = guess[baseTwoDigits(x)];
    return digits + (x >= tenToThe[digits]);
}

GCC 和 clang 在 x86 上将其编译为大约 10 条指令。小心,仍然可以在组装过程中加快速度。

关键的见解是使用(非常便宜的)以二为底的对数来快速估计以十为底的对数;在这一点上,我们只需要与一个 10 的幂进行比较,就可以决定是否需要调整猜测。这比搜索十的多次幂来找到正确的要有效得多。

如果输入压倒性地偏向一位数和两位数,则线性扫描有时会更快;对于所有其他输入分布,这种实现往往很容易获胜。

【讨论】:

  • + 非常好(假设int 足够大)。
  • @MikeDunlavey:是的,很明显,这不会扩展到任意精度的整数(尽管它会扩展到任何“小”固定大小的类型)。
  • 我已经将它推广到任何内置的无符号整数类型:coliru.stacked-crooked.com/a/16f8a901a31b9d73。你可能想用这个来更新你的代码。
  • 您的实现实际上甚至都不正确。 baseTenDigits(0) 产生 0,而它应该产生 1
  • 0 的日志是未定义的,所以我想这可以部分原谅 - 或者在需要时进行检查。主要思想是这是使用 base2 到达 base10 的一种非常好的方法。或者将 0 中的 0 更改为 4?表达式,然后 0 工作。
【解决方案2】:

一种方法是循环减去 10 的幂。这个幂可以计算并存储在表中。这里是python中的例子:

table = [10**i for i in range(1, 10)]
# [10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000]

def fast_log10(n):
    for i, k in enumerate(table):
        if n - k < 0:
           return i

使用示例:

>>> fast_log10(1)
0
>>> fast_log10(10)
1
>>> fast_log10(100)
2
>>> fast_log10(999)
2
fast_log10(1000)
3

您也可以对这个表使用二分搜索。那么算法复杂度将只有 O(lg(n)),其中 n 是位数。 这里是 C 中的二进制搜索示例:

long int table[] = {10, 100, 1000, 10000, 1000000};
#define TABLE_LENGHT sizeof(table) / sizeof(long int)

int bisect_log10(long int n, int s, int e) {
    int a = (e - s) / 2 + s;
    if(s >= e)
        return s;
    if((table[a] - n) <= 0)
        return bisect_log10(n, a + 1, e);
    else
        return bisect_log10(n, s, a);
}

int fast_log10(long int n){
    return bisect_log10(n, 0, TABLE_LENGHT);
}

注意对于小数字,这种方法会比上面的方法慢。 完整代码here.

【讨论】:

  • 线性搜索绝对不是“最快的方法”。
  • 您能否发布使用 O(1) 计算下限日志的方法的链接?
  • @StephenCanon:对——O(1) 方法是什么? [除非你考虑 n
  • @SlavaBacherikov:我知道,但如果你不把@Stephen放在评论里,他可能看不到。
  • @MikeDunlavey:我发布了一个带有无除法常数时间(最多二阶缓存效果)实现的答案。很抱歉花了这么长时间才回到这里。
【解决方案3】:

嗯,这是旧的备用 - “穷人的日志功能”。 (如果要处理超过 63 个整数,请将第一个“if”更改为“while”。)

n = 1;
if (v >= 1e32){n += 32; v /= 1e32;}
if (v >= 1e16){n += 16; v /= 1e16;}
if (v >=  1e8){n +=  8; v /=  1e8;}
if (v >=  1e4){n +=  4; v /=  1e4;}
if (v >=  1e2){n +=  2; v /=  1e2;}
if (v >=  1e1){n +=  1; v /=  1e1;}

因此,如果您输入 123456.7,情况如下:

n = 1;
if (v >= 1e32) no
if (v >= 1e16) no
if (v >=  1e8) no
if (v >=  1e4) yes, so n = 5, v = 12.34567
if (v >=  1e2) no
if (v >=  1e1) yes, so n = 6, v = 1.234567
     so result is n = 6

这是一个使用乘法而不是除法的变体:

int n = 1;
double d = 1, temp;
temp = d * 1e32; if (v >= temp){n += 32; d = temp;}
temp = d * 1e16; if (v >= temp){n += 16; d = temp;}
temp = d *  1e8; if (v >= temp){n +=  8; d = temp;}
temp = d *  1e4; if (v >= temp){n +=  4; d = temp;}
temp = d *  1e2; if (v >= temp){n +=  2; d = temp;}
temp = d *  1e1; if (v >= temp){n +=  1; d = temp;}

执行看起来像这样

v = 123456.7
n = 1
d = 1
temp = 1e32, if (v >= 1e32) no
temp = 1e16, if (v >= 1e16) no
temp =  1e8, if (v >=  1e8) no
temp =  1e4, if (v >=  1e4) yes, so n = 5, d = 1e4;
temp =  1e6, if (v >=  1e6) no
temp =  1e5, if (v >=  1e5) yes, so n = 6, d = 1e5;

【讨论】:

    【解决方案4】:

    如果你想要一个更快的日志函数,你需要近似他们的结果。例如。 exp 函数可以使用“短”泰勒近似来近似。您可以找到 exp、log、root 和 power here 的示例近似值

    编辑: 你可以找到一个简短的性能比较here

    【讨论】:

      【解决方案5】:

      因为无符号 &lt;&gt;= 测试只需减去并检查进位标志即可完成,因此可以将两个数组(guess 和取反的 tenToThe)放在一个 64 位值中,将两个数组查找合二为一,并使用 32 位加法的进位来调整猜测。 guess[n] 的高 32 位提供 log10(2^n*2-1) 的值,而低 32 位包含-10^log10(2^n*2-1)

      static unsigned int baseTwoDigits(unsigned int x) {
          return x ? 32 - __builtin_clz(x) : 0;
      }
      
      unsigned int baseTenDigits(unsigned int x) {
          static uint64_t guess[33] = {
            /* 1 */         0, 0, 0, 
            /* 8 */         (1ull<<32)-10, (1ull<<32)-10, (1ull<<32)-10, 
            /* 64 */        (2ull<<32)-100, (2ull<<32)-100, (2ull<<32)-100, 
            /* 512 */       (3ull<<32)-1000, (3ull<<32)-1000, (3ull<<32)-1000, 
                            (3ull<<32)-1000, 
            /* 8192 */      (4ull<<32)-10000, (4ull<<32)-10000, (4ull<<32)-10000, 
            /* 65536 */     (5ull<<32)-100000, (5ull<<32)-100000, (5ull<<32)-100000, 
            /* 524288 */    (6ull<<32)-1000000, (6ull<<32)-1000000, (6ull<<32)-1000000, 
                            (6ull<<32)-1000000, 
            /* 8388608 */   (7ull<<32)-10000000, (7ull<<32)-10000000,
                            (7ull<<32)-10000000, 
            /* 67108864 */  (8ull<<32)-100000000, (8ull<<32)-100000000, 
                            (8ull<<32)-100000000, 
            /* 536870912 */ (9ull<<32)-1000000000, (9ull<<32)-1000000000, 
                            (9ull<<32)-1000000000, 
          };
          uint64_t adjust = guess[baseTwoDigits(x)];
          return (adjust + x) >> 32;
      }
      

      【讨论】:

        【解决方案6】:

        不做具体说明,我只给出一个笼统的回答:

        日志功能在大多数语言中都非常有效,因为它是一个基本功能。

        您只对整数感兴趣这一事实可以为您提供一些优势,但这可能不足以轻松击败内置标准解决方案。

        我能想到的比内置函数更快的几件事之一是查找表,因此,如果您只对最多 10000 的数字感兴趣,您可以简单地创建一个可以用来在需要时查找这些值中的任何一个。

        显然,此解决方案无法很好地扩展,但它可能正是您所需要的。


        旁注:例如,如果您正在导入数据,实际上可能会更快地查看字符串长度(而不是先将字符串转换为数字然后再查看字符串的值)。当然,这将要求输入以正确的格式存储,否则它不会为您带来任何好处。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2015-10-08
          • 1970-01-01
          • 1970-01-01
          • 2018-06-12
          • 1970-01-01
          • 2021-02-09
          • 2012-10-25
          相关资源
          最近更新 更多