Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Round a number without using if statement

This is a matter of efficiency; I'm doing a lot of rounding in a tight loop and want to avoid conditional branching. I'm rounding to whole numbers.

The following numbers on the left should round to the numbers on the right:

1.2 -> 1.0, 1.5 -> 2.0, -1.2 -> -1.0, -1.5 -> -2.0

I have made a simple function which can round positive numbers given my above constraints.

  public static float RoundFast(float num)
  {
    return (int)(num + 0.5f);
  }

However, this will not produce desired results if num is negative. I was thinking about multiplying the 0.5f with the sign of num, however Math.Sign uses conditional branching from what I understand. I cannot bitshift a float in C# and am unaware of any way other to obtain the sign from the float without using branching.

How can I make this support rounding negative numbers as well?

like image 212
Slight Avatar asked Feb 18 '26 23:02

Slight


2 Answers

There is no way to achieve this with at least one if to determine whether the number is positive or negative.

If you want your own function, you can use this:

return num >= 0 ? (int)(num + 0.5f) : (int)(num - 0.5f);

...another possibility would be:

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);

...but still Math.Abs uses if statements internally. Also this adds an extra division and multiplication, and the first example uses only one if statement, which I believe it has a better performance.

Or you can use the built-in Math.Round() method, specifying a 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

Tests with custom method

I ran a test and here is the C# fiddle, and below it's the method:

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);
}

Results:

Compile:    0.301s
Execute:    0.012s~0.047s
Memory: 406.73kb~1.68Mb
CPU:    0.016s~0.078s

Tests with Math.Round

I also tested with Math.Round and here is the Fiddle, and the method:

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));
    }
}

Results:

Compile:    0.216s
Execute:    0.016s~0.031s
Memory: 494.41kb~1.68Mb
CPU:    0.062s

Conclusion

Both methods are using 4 use cases (two positives, two negatives), and executing the four 2.147.483.647 times, which is the int.MaxValue.

The difference between both methods being executed almost ten billion times in a row isn't that much, the custom method is a bit faster than Math.Round. I'd go for Math.Round anyway, but you can make your choice.

like image 158
Alisson Reinaldo Silva Avatar answered Feb 21 '26 13:02

Alisson Reinaldo Silva


I've run a bunch of the answers through BenchmarkDotNet tests. The full code of the tests is in this in this gist.

The test generates 100.000 pseudo-random floats between -2.5 and 2.5 and converts them to int in various ways.

The results are all within the same order of magnitude so I'd say for pretty much all common cases sticking to the platform standard Math.Round is perfectly ok.

When looking for the best performance, @Yves Daoust's code is the winner:

        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

A note on the code; yes, I used Linq to loop over the data but since I used the same linq statement in all samples I think it's safe to assume that it doesn't impact the result. I've executed this on my laptop in a .Net 4.6.1 console application built in Release configuration.

Update

I've added @harolds native code as requested in the comments end replaced the Linq code with a for-loop.

        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 |

Looks like Linq had a much bigger impact (around 1.5 ms) than I expected. I suspect the compiler can optimize the for loop much better but I'd love to hear from a more knowledgeable person about this.

The code for this test run is in another gist.

like image 44
Marnix van Valen Avatar answered Feb 21 '26 13:02

Marnix van Valen



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!