int X = a-b;
int d = Math.Abs(X);
我相当确定.NET不会进行内联。那么,我应该使用if()吗?还是有其他不太为人知的诀窍?
int X = a-b;
int d = Math.Abs(X);
我相当确定.NET不会进行内联。那么,我应该使用if()吗?还是有其他不太为人知的诀窍?
我进行了一些性能测试,以找出是否可以使用标准 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();
}
}
Math.Abs
函数,除非您在 .Net Framework 上,在这种情况下,请使用其他任一选项(我将使用第二个选项)。 - GuiorgyJIT在某些情况下会执行内联。我不知道它是否会将 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);
checked(-X)
- 很好。没想到。 - manojlds[Int32.MinValue +1, Int32.MaxValue - 1]
的所有整数值时,比起Math.Abs
函数,第一个表达式要快8倍。但是当我对-x
使用checked
时,对于完整的循环(包括正数和负数),加速只有4倍。当然,这取决于替换的Abs
函数需要多么通用,但鉴于checked
添加了相当多的开销,以确保对Int32.MinValue
的一致处理,放弃在大多数情况下使用checked
可能是值得的。 - Anders Gustafsson就算它的价值不高,32位带符号数采用2补码格式的绝对值通常是这样实现的:
abs(x) = (x^(x>>31))-(x>>31)
我只是查看是否小于零并乘以-1。
int d = (X < 0) ? (-X) : X;
Math.Abs
稍有不同。请参见:int.MinValue
。 - Anthony Pegram请参考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;
}
其他整数类型的重载函数类似。对于 float
和 double
的重载是外部调用,而 decimal
的重载使用自己的实现方式,构造一个新的实例。糟糕!
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
C# 支持内联 Math.Abs。下面的代码是可行的:
int x = 12;
int y = 17;
int z = Math.Abs(x - y);
Console.WriteLine(z); //outputs 5
Abs
函数调用被内联化了。 - LukeHC#内联了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
如果你知道这是一个最小化问题的差异,那么可以使用:a<b?b-a:a-b
sqrt(x^2)