不使用if语句舍入数字

时间:2017-05-21 20:03:35

标签: c# math optimization

这是一个效率问题;我在紧密的循环中进行了大量的舍入,并希望避免条件分支。我将四舍五入到整数。

左侧的下列数字应该是右边的数字:

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#中移位浮点数,并且不知道其他任何方式从浮点数中获取符号而不使用分支。

我如何才能支持舍入负数呢?

6 个答案:

答案 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进行测试

我还使用Math.Roundhere 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

.NET Fiddle Demo