20

We're doing a great deal of floating-point to integer number conversions in our project. Basically, something like this

for(int i = 0; i < HUGE_NUMBER; i++)
     int_array[i] = float_array[i];

The default C function which performs the conversion turns out to be quite time consuming.

Is there any work around (maybe a hand tuned function) which can speed up the process a little bit? We don't care much about a precision.

Mysticial
  • 464,885
  • 45
  • 335
  • 332
Serge
  • 7,706
  • 5
  • 40
  • 46
  • 1
    I'm surprised to hear there's a lot of code involved. I always assumed it was handled with a single instruction to the FPU. – rmeador Jan 09 '09 at 21:09
  • 3
    Even a single FPU instruction can take a lot of cycles. – Toon Krijthe Jan 09 '09 at 21:28
  • @Max Lybbert: x86, Windows, Linux, Mac OS – Serge Jan 10 '09 at 07:35
  • Some embedded systems don't have an FPU, so this kind of conversion has to go through software on those systems. However, I'm surprised to hear it's slow on x86. I take it you're using GCC? – Max Lybbert Jan 10 '09 at 08:10
  • What exact x86 processor (it makes a big difference)? How big is HUGE_NUMBER really (to reason about caching effect)? How many float_array elements can you process per second? – Mr Fooz Jan 12 '09 at 13:20
  • Pls. mark some answers as approved. There's a fair bit of misinformation on this page. I'd mark the SSE3 FISTTP mentions, and the magic number addition case (for non-SSE3 platforms). – akauppi Mar 15 '09 at 11:10
  • @akauppi: I didn't mean to creat a duplicate, but when I was about to ask the question I spend a while looking for similar ones didn't find any if I would I'd have never asked this one. What can I say SO search does suck... – Serge Mar 16 '09 at 09:20

15 Answers15

16

Most of the other answers here just try to eliminate loop overhead.

Only deft_code's answer gets to the heart of what is likely the real problem -- that converting floating point to integers is shockingly expensive on an x86 processor. deft_code's solution is correct, though he gives no citation or explanation.

Here is the source of the trick, with some explanation and also versions specific to whether you want to round up, down, or toward zero: Know your FPU

Sorry to provide a link, but really anything written here, short of reproducing that excellent article, is not going to make things clear.

Community
  • 1
  • 1
Larry Gritz
  • 13,331
  • 5
  • 42
  • 42
14
inline int float2int( double d )
{
   union Cast
   {
      double d;
      long l;
    };
   volatile Cast c;
   c.d = d + 6755399441055744.0;
   return c.l;
}

// this is the same thing but it's
// not always optimizer safe
inline int float2int( double d )
{
   d += 6755399441055744.0;
   return reinterpret_cast<int&>(d);
}

for(int i = 0; i < HUGE_NUMBER; i++)
     int_array[i] = float2int(float_array[i]);

The double parameter is not a mistake! There is way to do this trick with floats directly but it gets ugly trying to cover all the corner cases. In its current form this function will round the float the nearest whole number if you want truncation instead use 6755399441055743.5 (0.5 less).

deft_code
  • 57,255
  • 29
  • 141
  • 224
  • I don't think this does what you're expecting. The value in l will be the same bit pattern as in d, but it won't be anything similar to the same number: 6.054 != -9620726 (my machine, 32 bit little-endian). – Max Lybbert Jan 09 '09 at 21:34
  • @Max: The expectation is a 32-bit "long" type (and, of course, an IEEE-754 double). Given these, this works, although I doubt it could be any faster than "movsd xmm0, mmword ptr [d]; cvttsd2si eax, xmm0; mov dword ptr [i], eax" (which is what my compiler generates for the straight cast). – P Daddy Jan 09 '09 at 21:51
  • I learned this trick from the Lua source code. There are some places where it doesn't work, but I've never found one. It works fine on my core2duo and my old pentium. – deft_code Jan 09 '09 at 21:54
  • This seems like it would have a very low chance of working if the machine isn't exactly what you expect. Also I can't believe it would be better than other methods mentioned here. – Jay Conrod Jan 10 '09 at 03:06
  • That's Scary. I assume this is only vaid for a particular type of floating point number (IEEE-754???). I think you should make this explicit in your answer (unless it is true everywhere) an d also note that C++ does not specify a particular floating point standard so you should verify before use. – Martin York Jan 12 '09 at 07:38
  • 1
    On things that are not big iron and are not GPUs, IEEE-754 is where things are :) – Kuba hasn't forgotten Monica Feb 10 '13 at 21:20
  • 3
    Note that using a union for type-punning like this is legal in C11 but undefined behavior in all versions of C++ – Ben Voigt Dec 03 '15 at 17:57
  • 1
    Truncating by using 6755399441055743.5 does not seem to work in this case: http://cpp.sh/2dw45 – nspo Mar 01 '17 at 11:22
  • "if you want truncation instead use 6755399441055743.5" fails on 2 accounts: 6755399441055743.5 not exactly representable in IEEE - same as 6755399441055744.0. Conceptually does not truncate in the right direction for negative numbers. – chux - Reinstate Monica Aug 16 '20 at 08:01
  • 1
    Long is 64-bit on Linux x86-64, but 32-bit on Win64 and x86-32. Is it meant to be 32-bit or 64-bit or does it not matter? Could this answer be rewritten using int32_t or int64_t? – nyanpasu64 Oct 17 '20 at 06:20
8

I ran some tests on different ways of doing float-to-int conversion. The short answer is to assume your customer has SSE2-capable CPUs and set the /arch:SSE2 compiler flag. This will allow the compiler to use the SSE scalar instructions which are twice as fast as even the magic-number technique.

Otherwise, if you have long strings of floats to grind, use the SSE2 packed ops.

Crashworks
  • 40,496
  • 12
  • 101
  • 170
3

There's an FISTTP instruction in the SSE3 instruction set which does what you want, but as to whether or not it could be utilized and produce faster results than libc, I have no idea.

Matt Schmidt
  • 612
  • 3
  • 6
  • 1
    I think FISTTP will _automatically_ enhance the speed of the crippled casts '(int)float_val' via recompilation if: - SSE3 support is enabled ('-msse3' for gcc) - The CPU is SSE3 capable While the 'fix' is connected to SSE3 featureset, it is actually a X87 side feature. – akauppi Mar 15 '09 at 10:48
  • http://software.intel.com/en-us/articles/how-to-implement-the-fisttp-streaming-simd-extensions-3-instruction/ – akauppi Mar 15 '09 at 11:04
2

Is the time large enough that it outweighs the cost of starting a couple of threads?

Assuming you have a multi-core processor or multiple processors on your box that you could take advantage of, this would be a trivial task to parallelize across multiple threads.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Martin York
  • 257,169
  • 86
  • 333
  • 562
  • 1
    Your intention is fine. Only... think that using 4 cores, each repeatedly hick-uping its X87 pipeline because of the float->int conversion... Such a waste! :) SSE3 fix to the original silicon issue is the right thing (or the magic number addition trick, if SSE3 cannot be guaranteed). – akauppi Mar 15 '09 at 11:07
2

The key is to avoid the _ftol() function, which is needlessly slow. Your best bet for long lists of data like this is to use the SSE2 instruction cvtps2dq to convert two packed floats to two packed int64s. Do this twice (getting four int64s across two SSE registers) and you can shuffle them together to get four int32s (losing the top 32 bits of each conversion result). You don't need assembly to do this; MSVC exposes compiler intrinsics to the relevant instructions -- _mm_cvtpd_epi32() if my memory serves me correctly.

If you do this it is very important that your float and int arrays be 16-byte aligned so that the SSE2 load/store intrinsics can work at maximum efficiency. Also, I recommend you software pipeline a little and process sixteen floats at once in each loop, eg (assuming that the "functions" here are actually calls to compiler intrinsics):

for(int i = 0; i < HUGE_NUMBER; i+=16)
{
//int_array[i] = float_array[i];
   __m128 a = sse_load4(float_array+i+0);
   __m128 b = sse_load4(float_array+i+4);
   __m128 c = sse_load4(float_array+i+8);
   __m128 d = sse_load4(float_array+i+12);
   a = sse_convert4(a);
   b = sse_convert4(b);
   c = sse_convert4(c);
   d = sse_convert4(d);
   sse_write4(int_array+i+0, a);
   sse_write4(int_array+i+4, b);
   sse_write4(int_array+i+8, c);
   sse_write4(int_array+i+12, d);
}

The reason for this is that the SSE instructions have a long latency, so if you follow a load into xmm0 immediately with a dependent operation on xmm0 then you will have a stall. Having multiple registers "in flight" at once hides the latency a little. (Theoretically a magic all-knowing compiler could alias its way around this problem but in practice it doesn't.)

Failing this SSE juju you can supply the /QIfist option to MSVC which will cause it to issue the single opcode fist instead of a call to _ftol; this means it will simply use whichever rounding mode happens to be set in the CPU without making sure it is ANSI C's specific truncate op. The Microsoft docs say /QIfist is deprecated because their floating point code is fast now, but a disassembler will show you that this is unjustifiedly optimistic. Even /fp:fast simply results to a call to _ftol_sse2, which though faster than the egregious _ftol is still a function call followed by a latent SSE op, and thus unnecessarily slow.

I'm assuming you're on x86 arch, by the way -- if you're on PPC there are equivalent VMX operations, or you can use the magic-number-multiply trick mentioned above followed by a vsel (to mask out the non-mantissa bits) and an aligned store.

Crashworks
  • 40,496
  • 12
  • 101
  • 170
1

You might be able to load all of the integers into the SSE module of your processor using some magic assembly code, then do the equivalent code to set the values to ints, then read them as floats. I'm not sure this would be any faster though. I'm not a SSE guru, so I don't know how to do this. Maybe someone else can chime in.

FryGuy
  • 8,614
  • 3
  • 33
  • 47
  • SSE (or if you're cross-platform Altivec or Neon) will give you roughly the same speed as a memcopy. If bulk-conversion is aproblem the two-liner in assembly or intrinsic-based C is well worth the work. – Nils Pipenbrinck Jan 09 '09 at 21:06
1

In Visual C++ 2008, the compiler generates SSE2 calls by itself, if you do a release build with maxed out optimization options, and look at a disassembly (though some conditions have to be met, play around with your code).

1

See this Intel article for speeding up integer conversions:

http://software.intel.com/en-us/articles/latency-of-floating-point-to-integer-conversions/

According to Microsoft, the /QIfist compiler option is deprecated in VS 2005 because integer conversion has been sped up. They neglect to say how it has been sped up, but looking at the disassembly listing might give a clue.

http://msdn.microsoft.com/en-us/library/z8dh4h17(vs.80).aspx

Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
1

most c compilers generate calls to _ftol or something for every float to int conversion. putting a reduced floating point conformance switch (like fp:fast) might help - IF you understand AND accept the other effects of this switch. other than that, put the thing in a tight assembly or sse intrinsic loop, IF you are ok AND understand the different rounding behavior. for large loops like your example you should write a function that sets up floating point control words once and then does the bulk rounding with only fistp instructions and then resets the control word - IF you are ok with an x86 only code path, but at least you will not change the rounding. read up on the fld and fistp fpu instructions and the fpu control word.

starmole
  • 984
  • 7
  • 6
0

What compiler are you using? In Microsoft's more recent C/C++ compilers, there is an option under C/C++ -> Code Generation -> Floating point model, which has options: fast, precise, strict. I think precise is the default, and works by emulating FP operations to some extent. If you are using a MS compiler, how is this option set? Does it help to set it to "fast"? In any case, what does the disassembly look like?

As thirtyseven said above, the CPU can convert float<->int in essentially one instruction, and it doesn't get any faster than that (short of a SIMD operation).

Also note that modern CPUs use the same FP unit for both single (32 bit) and double (64 bit) FP numbers, so unless you are trying to save memory storing a lot of floats, there's really no reason to favor float over double.

Chris Smith
  • 5,326
  • 29
  • 29
  • That Visual Studio setting exists because reordering floating point math can produce slightly different results, even if it shouldn't mathematically, such as "a * (b + c)" vs "a*b + a*c". – Drew Dormann Jan 10 '09 at 02:23
  • See http://software.intel.com/en-us/articles/how-to-implement-the-fisttp-streaming-simd-extensions-3-instruction/ for how many instructions it takes to convert float->int (on X87). – akauppi Mar 15 '09 at 11:03
0

On Intel your best bet is inline SSE2 calls.

Sanjaya R
  • 6,246
  • 2
  • 17
  • 19
0

I'm surprised by your result. What compiler are you using? Are you compiling with optimization turned all the way up? Have you confirmed using valgrind and Kcachegrind that this is where the bottleneck is? What processor are you using? What does the assembly code look like?

The conversion itself should be compiled to a single instruction. A good optimizing compiler should unroll the loop so that several conversions are done per test-and-branch. If that's not happening, you can unroll the loop by hand:

for(int i = 0; i < HUGE_NUMBER-3; i += 4) {
     int_array[i]   = float_array[i];
     int_array[i+1] = float_array[i+1];
     int_array[i+2] = float_array[i+2];
     int_array[i+3] = float_array[i+3];
}
for(; i < HUGE_NUMBER; i++)
     int_array[i]   = float_array[i];

If your compiler is really pathetic, you might need to help it with the common subexpressions, e.g.,

int *ip = int_array+i;
float *fp = float_array+i;
ip[0] = fp[0];
ip[1] = fp[1];
ip[2] = fp[2];
ip[3] = fp[3];

Do report back with more info!

Norman Ramsey
  • 198,648
  • 61
  • 360
  • 533
  • It's not a simple instruction. See: http://software.intel.com/en-us/articles/how-to-implement-the-fisttp-streaming-simd-extensions-3-instruction/ (hi, Norman! Wouldn't have though of you.... ;) – akauppi Mar 15 '09 at 10:55
0

If you do not care very much about the rounding semantics, you can use the lrint() function. This allows for more freedom in rounding and it can be much faster.

Technically, it's a C99 function, but your compiler probably exposes it in C++. A good compiler will also inline it to one instruction (a modern G++ will).

lrint documentation

luispedro
  • 6,934
  • 4
  • 35
  • 45
  • Thanks for the info, but I've just run a quick test on mac with gcc-4.2 (with -O3) but it seems that lrint yields the same time as plain cast does. – Serge Sep 27 '11 at 08:49
  • I think gcc 4.2 might be too old. I know, from experience, that 4.1 did not yet do the inline (it used a function call). – luispedro Sep 27 '11 at 14:19
0

rounding only excellent trick, only the use 6755399441055743.5 (0.5 less) to do rounding won't work.

6755399441055744 = 2^52 + 2^51 overflowing decimals off the end of the mantissa leaving the integer that you want in bits 51 - 0 of the fpu register.

In IEEE 754
6755399441055744.0 =

sign exponent mantissa
0 10000110011 1000000000000000000000000000000000000000000000000000

6755399441055743.5 will also however compile to 0100001100111000000000000000000000000000000000000000000000000000

the 0.5 overflows off the end (rounding up) which is why this works in the first place.

to do truncation you would have to add 0.5 to your double then do this the guard digits should take care of rounding to the correct result done this way. also watch out for 64 bit gcc linux where long rather annoyingly means a 64 bit integer.

camelccc
  • 2,847
  • 8
  • 26
  • 52