快速整数绝对值函数

13
int X = a-b;
int d = Math.Abs(X);

我相当确定.NET不会进行内联。那么,我应该使用if()吗?还是有其他不太为人知的诀窍?


4
.NET框架确实进行内联优化。甚至有一种方法可以防止某个方法被内联:http://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.methodimploptions.aspx - Mike Dour
顺便提一下,我没有提到,我的X在0-255范围内,所以为了更好的效果,我可能可以使用查找表,只浪费510个字节,或者如果我想要更规范,就是510x4(整数表)。另外,是否有一个StackExchange网站专门用于“请优化这几行代码”,除了codereview.se.com? - Daniel Mošmondor
10个回答

27

我进行了一些性能测试,以找出是否可以使用标准 Math.Abs 以外的东西来节省时间。

执行这些测试共计 20 亿次(其中 i 的取值范围为 -1000000000 到 +1000000000,因此不会发生溢出),其结果如下:

Math.Abs(i)                    5839 ms     Factor 1
i > 0 ? i : -i                 6395 ms     Factor 1.09
(i + (i >> 31)) ^ (i >> 31)    5053 ms     Factor 0.86

(这些数字在不同的运行中略有变化)

基本上,你可以比使用 Math.Abs 得到微小的提高,但没有什么惊人之处。

使用位操作技巧可以缩短 Math.Abs 所需的时间,但可读性会严重受损。
使用简单分支可能反而更慢。总体而言,在我看来不值得。

所有测试都在 32 位操作系统、Net 4.0、VS 2010、发布模式下运行,未附加调试器。

以下是实际代码:

class Program
{
    public static int x; // public static field. 
                         // this way the JITer will not assume that it is  
                         // never used and optimize the wholeloop away
    static void Main()
    {
        // warm up
        for (int i = -1000000000; i < 1000000000; i++)
        {
            x = Math.Abs(i);
        }

        // start measuring
        Stopwatch watch = Stopwatch.StartNew();
        for (int i = -1000000000; i < 1000000000; i++)
        {
            x = Math.Abs(i);
        }
        Console.WriteLine(watch.ElapsedMilliseconds);

        // warm up
        for (int i = -1000000000; i < 1000000000; i++)
        {
            x = i > 0 ? i : -i;
        }

        // start measuring
        watch = Stopwatch.StartNew();
        for (int i = -1000000000; i < 1000000000; i++)
        {
            x = i > 0 ? i : -i;
        }
        Console.WriteLine(watch.ElapsedMilliseconds);

        // warm up
        for (int i = -1000000000; i < 1000000000; i++)
        {
            x = (i + (i >> 31)) ^ (i >> 31);
        }

        // start measuring
        watch = Stopwatch.StartNew();
        for (int i = -1000000000; i < 1000000000; i++)
        {
            x = (i + (i >> 31)) ^ (i >> 31);
        }
        Console.WriteLine(watch.ElapsedMilliseconds);


        Console.ReadLine();
    }
}

2
点赞,因为您实际执行了基准测试并提供了相关的基准测试环境细节。 - Special Sauce
1
我刚刚测试过,结果如下:.NET 4.5.2 32位:5224、3759、3782。 - Furkan Gözükara
2
在 x64 上:5764 4191 4026 - Furkan Gözükara
我的结果也与MonsterMMORPG相似。 - Chaitanya Gadkari
2
Ryzen 3800X,dotnet core 3.1:855,1407,950。 - joe
Windows,Ryzen 3600 .Net Core 3.1 x64: 861、961、972; x86: 858、962、969; .Net 5 x64: 963、1438、959; x86: 958、957、1436; .Net Framework 4.8 x64: 2155、967、967; x86: 2154、959、959; 结果是5次运行的平均值。 结论:只需使用 Math.Abs 函数,除非您在 .Net Framework 上,在这种情况下,请使用其他任一选项(我将使用第二个选项)。 - Guiorgy

18

JIT在某些情况下会执行内联。我不知道它是否会将 Math.Abs 内联,但您是否已经验证这是否实际上对您造成了性能问题?在您确定需要进行微小优化之前,请勿进行微观优化,然后测量类似以下内容的性能增益:

int d = X > 0 ? X : -X;

正如Anthony所指出的,上述方法在int.MinValue时通常无法运行,因为-int.MinValue == int.MinValue,而Math.Abs会抛出OverflowException异常。您还可以在纯C#代码中使用checked算术来强制执行此操作:

int d = X > 0 ? X : checked(-X);

3
那些干扰的最小值! - Anthony Pegram
3
checked(-X) - 很好。没想到。 - manojlds
@Voo:是的,我也这样期望。不过,在没有检查之前,我不喜欢对这种事情做出明确的说法 :) - Jon Skeet
3
在我的电脑上,当我遍历范围为[Int32.MinValue +1, Int32.MaxValue - 1]的所有整数值时,比起Math.Abs函数,第一个表达式要快8倍。但是当我对-x使用checked时,对于完整的循环(包括正数和负数),加速只有4倍。当然,这取决于替换的Abs函数需要多么通用,但鉴于checked添加了相当多的开销,以确保对Int32.MinValue的一致处理,放弃在大多数情况下使用checked可能是值得的。 - Anders Gustafsson
刚测试了一下,已检查和未检查的相等。它们比math.abs更快。 - Furkan Gözükara
显示剩余4条评论

7

就算它的价值不高,32位带符号数采用2补码格式的绝对值通常是这样实现的:

abs(x) = (x^(x>>31))-(x>>31)


啊,我怀疑这个。在Java中肯定不是这样实现的,而且由于在.NET中abs()类对于-2^31会抛出异常,所以那个技巧也行不通。 - Voo
您说得没错,如果没有任何范围或错误检查,.NET肯定不会以这种方式实现它,也不会有任何强大的数学库。但是,就像许多库调用一样,如果您知道不需要范围检查,可能可以编写执行更快的代码。 - Adam Smith
你的答案是真正正确的——如何编写快速的绝对值函数。而且这是最后一个。有一天我在寻找这个答案时,从“大师”或当地大学(比如Hans Passant和Little Jon)那里得到了很多废话……看来精英主义也不起作用。 - Boppity Bop
2
由另一个宇宙大师申请专利:http://www.patentgenius.com/patent/6073150.html - Hans Passant
2
"(x + (x >> 31)) ^ (x >> 31)" 这个表达式同样有效,而且没有被专利保护(尽管在法庭上专利可能无效)。请查看此页面上的评论:http://graphics.stanford.edu/~seander/bithacks.html#IntegerAbs - Leo

3

我只是查看是否小于零并乘以-1。

int d = (X < 0) ? (-X) : X;

3
这将会与 Math.Abs 稍有不同。请参见:int.MinValue - Anthony Pegram
@Anthony Pegram - 注意了。我试图实现 Java 中的 Abs :) - manojlds

2

请参考http://graphics.stanford.edu/~seander/bithacks.html#IntegerAbs,了解如何在不使用分支的情况下计算绝对值。

虽然.Net支持内联,但我怀疑编译器不会将Math.Abs()视为内联候选。以下是int重载的实现,由Reflector提供。

public static int Abs(int value)
{
  if (value >= 0)
    {
      return value;
    }
  return AbsHelper(value);
}

private static int AbsHelper(int value)
{
  if (value == -2147483648)
  {
    throw new OverflowException(Environment.GetResourceString("Overflow_NegateTwosCompNum"));
  }
  return -value;
}

其他整数类型的重载函数类似。对于 floatdouble 的重载是外部调用,而 decimal 的重载使用自己的实现方式,构造一个新的实例。糟糕!


2
如果你想要真正的性能,请尝试这个:
    int result = source & 0x7FFFFFFF;

它通过使用位AND运算符('&')并过滤掉符号位来工作,保留其余部分不变。
0x7FFFFFFF是(1<<31)的十六进制值,适用于int32(默认int)

有关位操作的更多信息,请访问此处:
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/bitwise-and-shift-operators

我在这个和其他函数中的时间如下(ryzen 3900x):

    Math.Abs(i):        2224ms
    i>0?i:-1   :         958ms
    (i+(i>>31))^(i>>31): 953ms
    i&0x7FFFFFFF:        486ms

1

C# 支持内联 Math.Abs。下面的代码是可行的:

int x = 12;
int y = 17;
int z = Math.Abs(x - y);
Console.WriteLine(z); //outputs 5

4
可以正常运行,但这并不能证明Abs函数调用被内联化了。 - LukeH

1

C#内联了Math.Abs(),这是C#和汇编代码(使用在线工具SharpLab生成)的Math.Abs

C#:

public int test(int n){
    return Math.Abs(n);
}

汇编语言:

L0000: push ebp
L0001: mov ebp, esp
L0003: test edx, edx
L0005: jge L000d
L0007: neg edx
L0009: test edx, edx
L000b: jl L0011
L000d: mov eax, edx
L000f: pop ebp
L0010: ret
L0011: call System.Math.ThrowAbsOverflow()
L0016: int3

0

如果你知道这是一个最小化问题的差异,那么可以使用:a<b?b-a:a-b


0
计算数学上的x的绝对值很简单:
sqrt(x^2)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接