C++ (n*2-1)%p:避免在n和p为32位时使用64位

C++ (n*2-1)%p:避免在n和p为32位时使用64位,c++,algorithm,c++11,math,modulus,C++,Algorithm,C++11,Math,Modulus,考虑以下功能: inline unsigned int f(unsigned int n, unsigned int p) { return (n*2-1)%p; } 现在假设n(和p)大于std::numeric\u limits::max() 例如f(4294967295U,4294967291U) 数学结果是7,但函数将返回2,因为n*2将溢出 那么解决方案很简单:我们只需使用64位整数即可。假设函数的声明必须保持不变: inline unsigned int f(unsign

考虑以下功能:

inline unsigned int f(unsigned int n, unsigned int p) 
{
    return (n*2-1)%p;
}
现在假设
n
(和
p
)大于
std::numeric\u limits::max()

例如
f(4294967295U,4294967291U)

数学结果是
7
,但函数将返回
2
,因为
n*2
将溢出

那么解决方案很简单:我们只需使用64位整数即可。假设函数的声明必须保持不变:

inline unsigned int f(unsigned int n, unsigned int p) 
{
    return (static_cast<unsigned long long int>(n)*2-1)%p;
}
inline unsigned int f(unsigned int n,unsigned int p)
{
返回(静态_-cast(n)*2-1)%p;
}
一切都很好。至少在原则上是这样。问题是这个函数在我的代码中会被调用数百万次(我指的是溢出版本),而64位模数比32位版本慢得多(参见示例)

问题是:是否有任何技巧(数学或算法)可以避免执行64位版本的模运算。使用这个技巧的新版本的
f
会是什么?(保留相同的声明)

  • 注1:
    n>0
  • 注2:
    p>2
  • 注3:
    n
    可以低于
    p
    n=4294967289U
    p=4294967291U
  • 注4:使用的模运算次数越少越好(3个32位模太大,2个很有趣,1的性能肯定会更好)
  • 注5:结果当然取决于处理器。假设在最后一台超级计算机上使用最后一个xeon

FWIW,此版本似乎可以避免任何溢出:

std::uint32_t f(std::uint32_t n, std::uint32_t p) 
{
    auto m = n%p;
    if (m <= p/2) {
        return (m==0)*p+2*m-1;
    }
    return p-2*(p-m)-1;
}
std::uint32\u t f(std::uint32\u t n,std::uint32\u t p)
{
自动m=n%p;

如果(m我们知道
p
小于
max
,那么
n%p
小于max。它们都是无符号的,这意味着
n%p
是正的,小于
p
。无符号溢出定义明确,因此如果
n%p*2
超过
p
,我们可以计算为
n%p-p+n%p
,它不会溢出,所以它一起看起来如下:

unsigned m = n % p;
unsigned r;
if (p - m < m) // m * 2 > p
    r = m - p + m;
else // m * 2 <= p
    r = m * 2;

// subtract 1, account for the fact that r can be 0
if (r == 0) r = p - 1;
else r = r - 1;
return r % p;

这使模运算的数量增加到1。

尽管我不喜欢处理AT&T语法和GCC的“扩展asm约束”,但我认为这是可行的(它在我的测试中起作用,当然是有限的)

我不知道,这些限制可能是不必要的严格或错误。它似乎起了作用


这里的想法是做一个常规的32位除法,实际上需要64位的红利。只有当商适合32位时(否则会发出溢出信号),它才起作用,在这种情况下总是如此(
p
至少2,
n
不是零)。除法之前的东西处理时间2(溢出到
edx
,即“高半”),然后“减去1”和潜在的借位。
“=d”
输出使其得到剩余部分。
“a”(n)
n
放入
eax
(让它选择其他寄存器没有帮助,除法将在
edx:eax
中进行输入).
“S”(p)
可能是
“r”(p)
(似乎有效),但我不确定是否足够信任它。

不过,你应该使用一个更好的例子,否则你会得到便宜的答案,利用
n
没有预先还原为什么不妥协?仅当答案可能溢出时(即,如果n>0x8000000),才进行64位模运算。如果你不经常用big
n
调用它,那么这会很好地工作。@nneonneo:我在帖子中加了一句话,这个函数经常用big
n
调用。顺便问一句,你对x86汇编的答案持开放态度吗?你可以回避大部分问题,因为32位模实际上需要64位除法(受一些限制,但如果
p>1
n!=0
,则在这里是可行的)请注意,这将非常依赖处理器…我使用下面的一些方法创建了一个快速基准测试(即使它们有缺陷,只是为了了解额外的复杂性是否比64位操作更昂贵).在我的旧Phenom II 955上,仅以64位进行计算甚至比32位稍好;如果n*2溢出,则切换到64位将多花约20%的成本,执行(n%p+n%p)会多花两倍的成本。Oth,在最近的i7上,如果32位需要1,则64位需要2,检查高位成本2.2,执行(n%p+n%p)成本1.4。如果p大于该类型所能代表的最大值的一半,则可能失败,如(n%p)*2仍然可以溢出。只有在那个例子中。翻转
n
p
…这样
n%p==n
@Peter现在代码似乎是正确的,有溢出吗?@Columbo这个想法是正确的。但是有几个小细节。
f(0,…)
返回
p-1
,而原始函数返回
((ulong)-1)%p
。另外,如果
m==p/2
,我们得到
(uint)-1
@AlexD第一部分的目的是。-1相当于p-1模p。但是,是的,我知道后者有问题,我已经解决了。现在呢?“64位整数不应该比64位平台上的32位整数慢。”这是不正确的,当谈到分区。编辑同意这篇文章,因为它与我的答案无关,无论如何我想知道<代码> R= M -P+M < /C++ >。<代码> M p>代码> 0以下,所以这不是导致溢出吗?因为<代码> R< /代码>是未签名的。我不熟悉C++。是的,<代码> M -P
确实会导致有意的下溢,但无符号整数的下溢是定义良好的。添加
m
会溢出回来,因为
m+m
在该分支中至少与
p
一样大。啊,很好。我不知道它会溢出回来。好主意。有没有办法将结果存储在n中而不是存储在res中以避免出现不一致的情况不必要的
return r >= p ? r - p : r
uint32_t f(uint32_t n, uint32_t p)
{
    uint32_t res;
    asm (
      "xorl %%edx, %%edx\n\t"
      "addl %%eax, %%eax\n\t"
      "adcl %%edx, %%edx\n\t"
      "subl $1, %%eax\n\t"
      "sbbl $0, %%edx\n\t"
      "divl %1"
      : "=d"(res)
      : "S"(p), "a"(n)
      : 
      );
  return res;
}