tl;dr:((n + 9) / 10) * 10 在更多情况下编译为最好(最快)的 asm 代码,对于了解 C 中整数除法的人来说,它易于阅读和理解。相当常见的成语。
对于需要与负数 n 一起工作的事物,我尚未调查最佳选择是什么,因为您可能希望从零舍入,而不是仍向 +Infinity 舍入,具体取决于应用程序。
查看不同建议使用的 C 操作,最轻量级的是 Mark Dickinson 的(以 cmets 为单位):
(n+9) - ((n+9)%10)
它看起来比几个人(包括@bta)建议的直接除法/乘法更有效:((n + 9) / 10) * 10,因为它只有加法而不是乘法。 (n+9 是一个公共子表达式,只需要计算一次。)
事实证明,两者都使用将division by a constant 转换为multiply and shift, see this Q&A for how it works 的编译器技巧编译成字面上相同的代码。与硬件div 指令不同,无论您使用商、余数还是两者结果都一样,mul/shift 方法需要额外的步骤来获得余数。所以编译器看到它可以通过更便宜的计算得到相同的结果,并最终将两个函数编译为相同的代码。
x86, ppc, and ARM 和我在 Godbolt 编译器浏览器上查看的所有其他架构都是如此。在这个答案的第一个版本中,我在 Godbolt 的 ARM64 的 gcc4.8 上看到了 %10 的 sdiv,但它不再安装(可能是因为它配置错误?)ARM64 gcc5.4 不这样做。
Godbolt 现在安装了 MSVC (CL),其中一些函数的编译方式不同,但我没有花时间看看哪个编译更好。
请注意,在 x86 的 gcc 输出中,乘以 10 很便宜,lea eax, [rdx + rdx*4] 得到 n*5,然后add eax,eax 加倍。 imul eax, edx, 10 在 Intel Haswell 上的延迟会高 1 个周期,但会更短(少一个 uop)。 gcc / clang 即使使用-Os -mtune=haswell 也不要使用它:/
接受的答案 (n + 10 - n % 10) 的计算成本甚至更低:n+10 可以与 n%10 并行发生,因此依赖链短了一步。它编译为少一条指令。
然而,它给出了 10 的倍数的错误答案:例如10 -> 20。建议的修复使用if(n%10) 来决定是否执行任何操作。这编译成cmov,所以它比@Bta 的代码更长更糟糕。如果您要使用条件,请这样做以获得负面输入的合理结果。
以下是所有建议答案的表现,包括否定输入:
./a.out | awk -v fmt='\t%4s' '{ for(i=1;i<=NF;i++){ a[i]=a[i] sprintf(fmt, $i); } } END { for (i in a) print a[i]; }'
i -22 -21 -20 -19 -18 -12 -11 -10 -9 -8 -2 -1 0 1 2 8 9 10 11 12 18 19 20 21 22
mark -10 -10 -10 -10 0 0 0 0 0 0 0 0 0 10 10 10 10 10 20 20 20 20 20 30 30
igna -10 -10 -10 0 0 0 0 0 10 10 10 10 10 10 10 10 10 20 20 20 20 20 30 30 30
utaal -20 -20 -20 -10 -10 -10 -10 -10 0 0 0 0 0 10 10 10 10 10 20 20 20 20 20 30 30
bta -10 -10 -10 -10 0 0 0 0 0 10 10 10 10 10 10 10 10 10 20 20 20 20 20 30 30
klatchko -10 -10 -10 -10 0 0 0 0 0 0 0 0 0 10 10 10 10 10 20 20 20 20 20 30 30
branch -10 -10 -20 0 0 0 0 -10 10 10 10 10 0 10 10 10 10 10 20 20 20 20 20 30 30
(transpose awk program)
Ignacio 的 n + (((9 - (n % 10)) + 1) % 10) 对负整数“正确”工作,向 +Infinity 舍入,但计算成本要高得多。它需要两个模运算,因此它的成本基本上是两倍。它编译成大约两倍的 x86 指令,执行的工作大约是其他表达式的两倍。
结果打印程序(与上面的godbolt链接相同)
#include <stdio.h>
#include <stdlib.h>
int f_mark(int n) { return (n+9) - ((n+9)%10); } // good
int f_bta(int n) { return ((n + 9) / 10) * 10; } // compiles to literally identical code
int f_klatchko(int n) { return n + 10 - n % 10; } // wrong, needs a branch to avoid changing multiples of 10
int f_ignacio(int n) { return n + (((9 - (n % 10)) + 1) % 10); } // slow, but works for negative
int roundup10_utaal(int n) { return ((n - 1) / 10 + 1) * 10; }
int f_branch(int n) { if (n % 10) n += (10 - n % 10); return n; } // gcc uses cmov after f_accepted code
int main(int argc, char**argv)
{
puts("i\tmark\tigna\tutaal\tbta\tklatch\tbranch");
for (int i=-25 ; i<25 ; i++)
if (abs(i%10) <= 2 || 10 - abs(i%10) <= 2) // only sample near interesting points
printf("%d\t%d\t%d\t%d\t%d\t%d\t%d\n", i, f_mark(i), f_accepted(i),
f_ignacio(i), roundup10_utaal(i), f_bta(i), f_branch(i));
}