这是一个效率问题;我在紧密的循环中进行了大量的舍入,并希望避免条件分支。我将四舍五入到整数。
左侧的下列数字应该是右边的数字:
1.2 - > 1.0,1.5 - > 2.0,-1.2 - > -1.0,-1.5 - > -2.0
我已经制作了一个简单的函数,可以根据上述约束对正数进行舍入。
public static float RoundFast(float num)
{
return (int)(num + 0.5f);
}
但是,如果num
为否定,则不会产生预期效果。我在考虑将0.5f
乘以num
的符号,但Math.Sign
使用我理解的条件分支。我不能在C#中移位浮点数,并且不知道其他任何方式从浮点数中获取符号而不使用分支。
我如何才能支持舍入负数呢?
答案 0 :(得分:3)
至少有一个if
无法确定此数字是正数还是负数。
如果您需要自己的功能,可以使用:
return num >= 0 ? (int)(num + 0.5f) : (int)(num - 0.5f);
......另一种可能性是:
var a = Math.Abs(num);
var b = a / num; // "b" will be -1 for negative numbers and 1 for positive...
return (int)((a + 0.5f) * b);
...但仍然Math.Abs
在内部使用if statements
。这也增加了额外的除法和乘法,第一个例子只使用了一个if
语句,我相信它具有更好的性能。
或者您可以使用内置的Math.Round()
方法,指定MidpointRounding
:
Math.Round(1.2, MidpointRounding.AwayFromZero); // 1
Math.Round(1.5, MidpointRounding.AwayFromZero); // 2
Math.Round(-1.2, MidpointRounding.AwayFromZero); // -1
Math.Round(-1.5, MidpointRounding.AwayFromZero); // -2
我跑了一个测试here is the C# fiddle,下面是方法:
public static void Main()
{
for (var i = 0; i < int.MaxValue; i++) {
Console.WriteLine(Round(1.2));
Console.WriteLine(Round(1.5));
Console.WriteLine(Round(-1.2));
Console.WriteLine(Round(-1.5));
}
}
public static int Round(double num)
{
return num >= 0 ? (int)(num + 0.5f) : (int)(num - 0.5f);
}
结果:
Compile: 0.301s
Execute: 0.012s~0.047s
Memory: 406.73kb~1.68Mb
CPU: 0.016s~0.078s
我还使用Math.Round
和here is the Fiddle以及方法
public static void Main()
{
for (var i = 0; i < int.MaxValue; i++) {
Console.WriteLine(Math.Round(1.2, MidpointRounding.AwayFromZero));
Console.WriteLine(Math.Round(1.5, MidpointRounding.AwayFromZero));
Console.WriteLine(Math.Round(-1.2, MidpointRounding.AwayFromZero));
Console.WriteLine(Math.Round(-1.5, MidpointRounding.AwayFromZero));
}
}
结果:
Compile: 0.216s
Execute: 0.016s~0.031s
Memory: 494.41kb~1.68Mb
CPU: 0.062s
两种方法都使用了4个用例(两个正数,两个负数),并执行了四个2.147.483.647次,即int.MaxValue
。
连续执行近百亿次的两种方法之间的差异并不大,自定义方法比Math.Round
快一点。无论如何,我会选择Math.Round
,但你可以做出选择。
答案 1 :(得分:3)
我通过BenchmarkDotNet测试运行了一堆答案。测试的完整代码在this gist中。
测试在-2.5和2.5之间生成100.000个伪随机浮点数,并以各种方式将它们转换为int
。
结果都在同一个数量级内,所以我要说几乎所有坚持平台标准Math.Round
的常见情况都是完全可以的。
在寻找最佳表现时,@ Yves Daoust的代码是赢家:
Method | Mean | Error | StdDev |
------------- |---------:|----------:|----------:|
Round | 5.539 ms | 0.0545 ms | 0.0483 ms | Math.Round
AddAndCast | 1.678 ms | 0.0356 ms | 0.1045 ms | OPs original code
AddAndCast2 | 2.422 ms | 0.0478 ms | 0.0701 ms | Yves Daoust's code
RoundFast | 4.494 ms | 0.0310 ms | 0.0274 ms |
ConvertToInt | 3.890 ms | 0.0279 ms | 0.0233 ms | Convert.ToInt32
关于代码的说明;是的,我使用Linq循环数据,但由于我在所有样本中使用相同的linq语句,我认为可以安全地假设它不会影响结果。 我已经在我的笔记本电脑上在发布配置中构建的.Net 4.6.1控制台应用程序中执行了此操作。
<强>更新强>
我在评论结束时添加了@harolds本机代码,用for循环替换了Linq代码。
Method | Mean | Error | StdDev |
------------- |------------:|-----------:|-----------:|
Round | 4,085.99 us | 78.7908 us | 77.3831 us |
AddAndCast | 151.86 us | 2.9784 us | 2.9252 us |
AddAndCast2 | 774.20 us | 12.8013 us | 11.9743 us |
RoundFast | 3,043.38 us | 52.9597 us | 49.5386 us |
ConvertToInt | 2,567.69 us | 44.3540 us | 41.4887 us |
Native | 26.20 us | 0.4291 us | 0.4014 us |
看起来Linq的影响比我预期的要大得多(大约1.5毫秒)。我怀疑编译器可以更好地优化for循环,但我很乐意听到一个知识渊博的人对此有所了解。
此测试运行的代码位于another gist。
答案 2 :(得分:2)
以下陈述并非不可能通过条件分配实施,因此无分支。但是你需要检查生成的程序集:
return num >= 0 ? (int)(num + 0.5f) : (int)(num - 0.5f);
您可以自行承担风险,也可以破解IEEE浮点表示,并使用0.5f
的FP表示替换除符号之外的所有位。
模拟联合类型:
[StructLayout(LayoutKind.Explicit)]
struct Union { [FieldOffset(0)] public float x; [FieldOffset(0)] public int i; };
调整位
Union U;
U.i = 0;
U.x= x;
U.i = 1056964608 | (-2147483648 & U.i); // Transfer the sign bit to 0.5f
int i = (int)(x + U.x);
请注意,添加赋值U.i= 0
只是为了取悦编译器(相信未分配的字段)。在进行基准测试时,应该对所有人进行一次。
答案 3 :(得分:2)
如果允许本机导入,可以在单独的C ++项目中编写:
extern "C" {
__declspec(dllexport) void roundAll(int len, float* data)
{
int i;
for (i = 0; i < len - 3; i += 4)
{
__m128 d = _mm_loadu_ps(data + i);
d = _mm_round_ps(d, _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC);
_mm_storeu_ps(data + i, d);
}
for (; i < len; i++)
{
__m128 d = _mm_load_ss(data + i);
d = _mm_round_ss(d, d, _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC);
_mm_store_ss(data + i, d);
}
}
}
像这样导入:
[DllImport("nativeFunctions.dll")] // or however you call your dll
static extern void roundAll(int len, float[] data);
在我的测试中,使用简单的方法,它在C#中的速度大约是其14倍
for循环中data[i] = (float)Math.Round(data[i])
,但结果将取决于数组的大小,您使用的.NET版本,以及是否生成32位代码或64位代码。
答案 4 :(得分:0)
public static float RoundFast(float num)
{
return Convert.ToInt32( num );
}
答案 5 :(得分:0)
这应该没有任何条件分支:
public static float RoundFast(float num)
{
return (int) (num + 0.5f * (-1 + 2 *(1 + num - (num + 1) % num) / num));
}
签名检测部分的积分转到:Get the sign of a number in C# without conditional statement