【问题标题】:How to evaluate zero sets fast?如何快速评估零集?
【发布时间】:2016-08-26 22:01:48
【问题描述】:

最近的code golfing post 询问了以下在 C 中快速实现的可能性(假设n 是一个无符号整数):

if (n==6 || n==8 || n==10 || n==12 || n==14 || n==16 || n==18 || n==20)

一种可能的简化方法是观察数字a[]={6,8,10,12,14,16,18,20} 形成一个arithmetic progression,因此移动范围,然后使用一些bitwise tricks

if (((n - 6) & 14) + 6 == n)

导致实现更短(并且可能确实更有效),如 John Bollinger 的answered

现在我要问的是什么是类似优雅(并且希望同样有效)的实现

if (n==3 || n==5 || n==11 || n==29 || n==83 || n==245 || n==731 || n==2189)

提示:这一次数字a[k] 形成一个几何级数a[k]=2+3^k

我想在一般情况下,最好的办法是对数字 a[k] 进行排序,然后进行对数搜索以测试 n 是否是排序数组的成员。

【问题讨论】:

  • 如果这是一个时间超过空间的权衡,那么我将分配一个 2189 字的缓冲区,除了特定的位置(3、5、11 等)之外,用零填充,然后简单地执行数组查找(这是非常快的,因为它是硬件实现的)。这不是很优雅,但会以空间性能为代价为您提供最佳时间性能。
  • 这样的问题不应该发到codereview.stackexchange.com吗?
  • FWIW,((n - 6) & 14) + 6 == n 可以简化为(n - 6) | 14 == 14
  • @UriBrecher - 对于 2k 值,也许。对于一般情况,巨大的查找表对缓存的影响可能不可忽略。
  • 您是追求原始速度还是外观优雅的 C 代码?它们很可能是不同的。除法(包括模)运算是现代处理器上最慢的运算,因此 thiru 的答案看起来很酷,但可能不会比一系列比较快。如果您确实关心速度,请在做出决定之前检查编译器的输出和 benchmark。还要考虑分支在紧密循环中很慢。如果您的优化器不够智能,无法自动执行此替换(Clang 是,其他不是),请考虑使用按位 OR 而不是逻辑 OR。

标签: c++ c performance sorting search


【解决方案1】:

这和recognizing a power of three非常相似,你可以适应例如this solution

bool test(unsigned x) {
    x -= 2;
    if (x > 2187)
        return 0;
    if (x > 243)
        x *= 0xd2b3183b;
    return x <= 243 && ((x * 0x71c5) & 0x5145) == 0x5145;
}

在给定的范围内,这可以进一步简化(通过蛮力找到):

bool test2(unsigned x) {
  x -= 2;
  return x <= 2187 && (((x * 0x4be55) & 0xd2514105) == 5);
}

【讨论】:

  • 见鬼,这是一些好东西。你能用几行详细说明你是如何从“基本”版本到“高级”版本的吗?这里有什么大创意?
【解决方案2】:

提示:这次数字a[k] 形成几何级数:a[k]=2+3^k

n = 2 + 3^k
n - 2 = 3^k

(n - 2) / 3^k = 1
(n - 2) % 3^k = 0

k = 0 ~ n-2 = 3^0 = 1, n = 3
k = 1 ~ n-2 = 3^1 = 3, n = 5
k = 2 ~ n-2 = 3^3 = 9, n = 11

if (n > 2 && isPow(n-2, 3))

带有函数isPow(x,y)的定义

bool isPow(unsigned long x, unsigned int y)
{
    while (x % y == 0)
    {
        x /= y;
    }

    return x == 1;
}

n = n  ~ n-2 = 3^k/3 = 3^(k-1)/3 = .. = 3^1/3 = 1%3 != 0
n = 11 ~ n-2 = 9/3 = 3/3 = 1%3 != 0
n = 5  ~ n-2 = 3/3 = 1%3 != 0
n = 3  ~ n-2 = 1%3 != 0

同样我们可以扣除k..

int k = findK(n-2, 3);

int findK(unsigned long x, unsigned int y)
{
    unsigned int k = 0;

    while (x % y == 0)
    {
        x /= y;
        k++;
    }

    if (x == 1) return k;
    else return (-1);
}

n - 2 = 3 * 3^(k-1)       for k > 0
(n - 2) / 3 = 3^(k-1)
(n - 2) / 3 / 3 = 3^(k-2)
(n - 2) / 3 / 3 / 3 = 3^(k-3)
(n - 2) / 3 / 3 / 3 / 3 = 3^(k-4)
..
(n - 2) / 3 / 3 / ..i times = 3^(k-i)
..
(n - 2) / 3 / 3 / ..k times = 3^0 = 1

【讨论】:

  • 具有与线性搜索相同的时间复杂度。
  • @n.m.我要去analogously elegant
  • 在您的第一个代码中:为什么将3* 向左移动成为/ 3,然后在下一步再次向右移动并向左移动3^(k-1) 术语?您还重复了公式(n - 2) % 3^k = 0 两次。
  • @Bakuriu 是的,重复的等式是错字,33^(k-1) 交换以展示如何在函数 isPow(x,y) 中应用 (n - 2) % 3^k = 0
【解决方案3】:

一种非常有效的方法是使用集合,尤其是unordered_set。作为哈希表,搜索是平均常数,最差的线性。它也比一系列条件优雅得多,并且可以很好地扩展。

只需插入要比较的值,然后 count 插入集合中的值。如果找到,它就是您的测试值之一。

std::unordered_set< int > values;
values.insert( 3 );
values.insert( 5 );
values.insert( 11 );
values.insert( 29 );
values.insert( 83 );
values.insert( 245 );
values.insert( 731 );
values.insert( 2189 );

...

if( values.count( input ) )
    std::cout << "Value is in set.\n";
else
    std::cout << "Value is NOT in set.\n";

【讨论】:

  • 我喜欢这种解决问题的方法。继续让我惊讶的是,在这里你没有利用数字的结构,也就是说,它们不仅仅是一些随机数,而是它们形成了几何级数。您在这里声称,这些庞大的信息对性能没有任何影响,这当然可能是真的,但它看起来对我来说是违反直觉的。
  • @Matsmath 好吧,关于这些数字还有另一个同样重要的信息被利用数学级数忽略了:它们如何影响散列函数,以及它们在集合中的处理方式。也就是说,您也许可以利用系列进展来编写自定义散列函数;这可能允许完全消除碰撞,从而强制不断搜索。但是,这主要是学术观察,我认为没有人会为这种事情这样做。
  • @WilliamKappler 我不同意你的最后一句话:en.wikipedia.org/wiki/Perfect_hash_function。在极少数情况下,需要这样做。
  • 让我澄清一下,“这类事情”是指在替换链式条件而不是作为一个整体的自定义哈希函数的上下文中使用一个。为大约 10 个元素编写自定义散列函数似乎真的过分了。但在某些情况下,自定义函数肯定会在更广泛的环境中发挥作用。
【解决方案4】:
if ((n > 2) && (2187 % (n - 2) == 0))

检查(n - 2) 是否是3 的幂并且小于或等于2187(3 的 7 的幂)

概括地说,要检查任何无符号整数n 是否是素数k 的幂,您可以检查n 是否除以可以存储在无符号整数中的k 的最大幂。

【讨论】:

  • 这是一个不错的答案,但您应该将“小于或等于 2187”更改为“小于或等于 2189”。另外,您应该添加n &gt; 2的初步验证。
  • @thiru 干得好! (我可以通过测试确认您的解决方案是正确的并且速度更快(1.14703 ns,与 3.34201 ns 相比)
  • 请注意除法运算(包括模运算)基本上是现代处理器上最慢的指令,比比较慢得多。因此,尽管这是编写代码的一种简洁方式(并且被授予,正是提问者正在寻找的答案),但它可能不是最快的代码。
  • @CodyGray 32 位整数除法比比较慢得多,但应该比单个错误预测的跳转快
  • 让我们接受这类似的优雅。谢谢你的回答。
【解决方案5】:

这是我想出的:

bool b;
if (n >= 3 && n <= 2189)
{
    double d = std::log(n - 2)/std::log(3); // log₃(n - 2)
    b = std::trunc(d) == d; // Checks to see if this is an integer
    // If ‘b’ then ‘n == a[log₃(n - 2)]’
}
else b = false;

if (b)
{
    // your code
}

由于数字n 是一个小整数,因此浮点不准确应该不是问题。

当然,它不会像整数运算那样快,它也不适合表达式,它可能比某种数组搜索更快,特别是如果你在哪里增加最大 n 值。

EDIT 我测试了我的代码(未启用优化),平均而言(对于所有小于或等于 2189 的 n)我的版本耗时 185.849 ns,而 || 版本耗时 116.546 ns,(多次运行我的程序会产生类似的结果)所以这不是更快。 同样出于某种奇怪的原因,当n == 245 时,它将b 设置为false,(应该是真的)。​​

【讨论】:

  • 如果n &lt; 2 怎么办?顺便说一句,计算单个 log 可能需要比原始表达式更多的内容。至少对于log(3),由于问题是关于性能的,您可能应该将其保存在static 变量中(即,只计算一次)。
  • @barakmanos 谢谢我解决了这个问题。嗯,我没有想到,也许我会测试一下......
  • 第二个想法,带负数的log 可能返回-inf of NaN,在这种情况下,接下来的测试可能会起作用。
  • static double log3 = std::log(3).
  • 你为什么要在没有优化的情况下测试它?这几乎说明了哪个更快没有实际意义。
【解决方案6】:

在相关帖子中发现了类似的问题: 你可以使用std::find

bool found = (std::find(my_var.begin(), my_var.end(), my_variable) != my_var.end());
// my_var is a list of elements.

(确保包含 )。

对于这类东西,在 99% 的情况下,有一个库可以为您完成这项工作。

【讨论】:

  • std::find 具有线性运行时间。我认为这算不上最快的实现......
  • 对于 8 个值,这可能很快——许多 CPU 都有一个重复比较操作,带有一个计数器来限制搜索的数据量,而且缓存的数据不多。如果这优于二分搜索,我一点也不感到惊讶。
  • @TonyD 我认为在其中一个 C++ 会议上有一个演示文稿,在该会议上实际测量了它,并且对于小型集合,线性搜索更快。但是,这个问题也要求一般情况,他正在计算汇编指令。在这种人为的情况下,我认为线性搜索不符合条件。
  • @Matsmath 在这个行业中,我们以事实而非感觉行事。
  • @Matsmath 为什么包含库会影响性能?无论如何,函数调用可能会被内联。性能与手写循环没有什么不同。
猜你喜欢
  • 1970-01-01
  • 2011-04-22
  • 1970-01-01
  • 1970-01-01
  • 2011-05-31
  • 2020-09-10
  • 2010-12-19
  • 2013-09-27
  • 1970-01-01
相关资源
最近更新 更多