1
\$\begingroup\$

I use the following MY_ROUND macro for rounding:

#define MY_SIGN(inVal) (((inVal)>=0)? 1 : -1)

#define MY_ROUND(inVal,numDig) (((double)(int)((inVal)*pow((double)10,numDig)+MY_SIGN(inVal)*0.5))/pow((double)10,numDig))

Here inVal is the input double to be rounded and numDig is the number of digits (precision) to round for.

Due to the divisions and pow()'s this function is quite slow. Is there any better replacement for this which does not waste that much time?

\$\endgroup\$
5
  • \$\begingroup\$ What is CNCO_SIGN? My compiler (Clang) does not know that. \$\endgroup\$
    – Martin R
    Commented Sep 7, 2016 at 12:24
  • \$\begingroup\$ Sorry, I added it! \$\endgroup\$
    – Elmi
    Commented Sep 7, 2016 at 12:28
  • \$\begingroup\$ Rounding by adding/subtracting 0.5 is incorrect. It does not produce correct results for a large number of floats. \$\endgroup\$
    – user34867
    Commented Sep 7, 2016 at 13:47
  • 2
    \$\begingroup\$ You lost me at Macro. There is no reason to use such arcane tricks in modern C++ there are much better techniques (called functions). Are there better yes just looks a <cmath> for rounding functions trunc() ceil() floor() round(). All the difference specializations for rounding are already provided and implemented in the fastest way for your specific platform. All you would need to do is add the bit to cope with the number of digits. \$\endgroup\$ Commented Sep 7, 2016 at 16:20
  • 1
    \$\begingroup\$ It's sort of funny how many answers you got, while the fundamental problem remains, your code does not do, what you think it does. :) \$\endgroup\$
    – Ped7g
    Commented Sep 8, 2016 at 14:33

4 Answers 4

2
\$\begingroup\$

I'd go with an inline function, as @pacmaninbw said, instead of a macro. It's more readable and it's easier to notice things that you may want to optimize. A couple of things that may speed this up a bit if you use a function:

  • It would be easier to store the result of pow((double)10,numDig) in a variable, so you can compute that only once (you're doing it twice, now). While you're at it, you may think about pre-computing a few values, say up to 10^26 and use a lookup table. I'm not really sure that it'd be faster for such a simple operation, though.

  • The MY_SIGN macro only returns -1 or 1, then you multiply the result by 0.5. These are hard-coded values, so why not having something that returns -0.5 or 0.5 instead ?

\$\endgroup\$
2
\$\begingroup\$

You can't.

For example you can't round 0.31 to 1 decimal digit 0.3 as an double with current HW, as the double is not capable to store 0.30 exactly.

Try for example:

const double x = 0.3;
printf("%.20lf\n", x);

output:
0.29999999999999998890

(the exact value stored in x is 0.299999999999999988897769753748434595763683319091796875, encoded as 0x3fd3333333333333 64 bit integer value)

So you are just destroying precision bits of the original value, but the resulting value can still contain decimals with precision you don't expect.

For some more information study how the double floating point numbers are designed, maybe this may be of help: https://en.wikipedia.org/wiki/Floating_point#Accuracy%5Fproblems


To get forward from this situation, there are two basic paths possible:

1) keep using HW floating point numbers

This has huge performance bonus, so that's why the float/double are first choice citizens in C++ floating point number calculations. Usually the tiny inaccuracy of results is small price to pay for the HW boost of calculations.

Then you should arrange your calculations in such way, that the cumulative error is smallest possible, and store around the double values with maximum possible precision for any record purpose.

You then round the value to proper number of decimal places only during output formatting, for example like sprintf(char_buffer, "%.2lf", value); to get the double rounded to two decimal places in form of "string".

Of course you should be aware of total possible cumulative error, and make sure it's within the acceptable level of inaccuracy for your software.

2) use other encoding of your numbers: some big numbers library

One of the possibilities is to use some library for arbitrary precision [decimal] numbers (like Java's BigInteger class, can't recall any C++ one from head).

These encode numbers in some custom binary way (or even as string), and have their own versions of common math operations, working with these numbers. Usually imitating human base 10 number format - which is not perfect either! For example you have two forms for every integer number: 1.0 = 0.9999..., or numbers like Pi can't be written down without infinite number of decimal places.

These work like charm for financial software, where human base 10 formatting is actually perfect fit, with all it's quirks and imperfections.

The price is of course the performance, as each operation is emulated by several HW instructions, so even thing like simple addition may be 10-100 times slower than the HW double + double.

3) use other encoding of your numbers: fixed math

When you need only exact number of decimal places, like for example you want uniform distribution of space coordinates, you can use integers, and assign some bits for decimal part. For example 16b unsigned short can be split into 8:8 whole:decimal part, supporting numbers from 0.0 to 255.99609375 (255 + 255/256), with constant 256 (inclusive) granularity between every two integer values. It may look a bit confusing at first, but these are easy to use in ASM with bit shifting (to get the whole part of number you only shift the value by 8 bits to right, to add two values you simply add them as two integers, etc).

This is also sometimes used for financial software to calculate amounts with 2 decimal places, this is done not by allocating exact number of bits for the decimal part, but by simply having all values multiplied by 100, i.e. $3.59 is stored internally as 359 integer, and when the value is displayed to human, it is temporarily formatted as "$(value div 100).(value mod 100)".

The limitation of this method is, that you have to decide the fixed precision for particular values, so it's not universal silver bullet. But compared to the big number libraries, the fixed math has performance very close to HW floating point numbers, on some CPU architectures it may be actually faster (like back in ages Intel 80486).


So whatever you plan use that macro for, you should probably stop and look into your SW architecture, as it doesn't make sense.

If you want to use it for output formatting, I would suggest to use rather the already created and debugged C/C++ facilities, like printf. If you still insist to do it by your own, at least produce some kind of string, not double, which would immediately destroy your rounding (as I shown at the beginning of my answer).

\$\endgroup\$
1
\$\begingroup\$

To be honest, I don't know if there is a way to speed it up. I do know that a lot of C++ programmers would look at this and cringe because macros really aren't used so much in C++ any more, also you're using old style C casts rather than C++

    static_cast<TYPE>(VALUE).

A macro isn't a function, it is replaced by the C/C++ pre-processor prior to the actual compilation of the code.

One of the reasons macros are not common in C++ any more is because they aren't type safe.

There are a couple of other ways to achieve what macros do for you in C++, one is to use template functions, and the other is to inline the function. Template functions accept various types and perform the same function, inline functions replace the function call with the actual generated code in the calling code. You may want to look at this stackoverflow question and this one as well.

Static casts evaluate the cast at compile time to see if the cast can actually be performed, dynamic casts do the type checking at run time. You may want to look at this stackoverflow question.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ It is not necessary to keep this macro, it is just inherited from some legacy code. So whatever the solution is, when it is faster than this piece of code it will be accepted (except inline-assembler, this causes too much troubles with different compilers) \$\endgroup\$
    – Elmi
    Commented Sep 7, 2016 at 12:35
  • \$\begingroup\$ I'm talking about inline C++ functions, not assembly, it's a recommendation to the compiler which may be ignored in some cases. \$\endgroup\$
    – pacmaninbw
    Commented Sep 7, 2016 at 12:38
0
\$\begingroup\$

As others have already noted, it's probably better to use an inline function to do this job in C++.

Assuming a reasonably recent standard library, you can also carry out the rounding quite a bit more cleanly using the round function, to get code something like this:

template <class T>
T my_round(T input, int digits) { 
    T factor = pow(T(10), digits);

    return round(input * factor) / factor;
}

As to efficiency: yes, this can be significantly faster (but, as always when dealing with performance, only testing will show for sure). The main reason for the speed improvement is that rounding is often handled as a mode of operation for the floating point unit. When you convert to int (by truncating) the compiler has to generate code to change the current mode to truncate the number, then convert to an int, and finally change the mode back to round (as it needs it while doing normal calculation). These mode changes tend to be quite expensive, so code like above that avoids them can be faster.

This does not reproduce the behavior of the macro in all respects. The macro produces undefined behavior if the number is too large to represent as an int. For example, MY_ROUND(1.23456e+30, 3) produces -2.1478e+06 on my machine (but it's undefined behavior, so don't be surprised if you get something different). For the same input, this function produces 1.23456e+30. That may not be what's desired (it still includes more digits than were specified) but at least it's not obviously wrong.

Neither this nor the original macro deals well with numbers less than 1.0. For example, if either this or your macro is asked to round 1.23456e-30 to 3 digits, both produce 0, rather than the 1.23e-30 that I'd hope for. For better or worse, however, your macro has defined behavior in this case, so I didn't feel as free to "fix" it to get the behavior I think is more desirable.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.