View Full Version : About the slowness of lrint() function


LoRd_MuldeR
3rd February 2017, 18:59
I noticed that the lrint() (http://www.cplusplus.com/reference/cmath/lrint/) function (round floating-point value to nearest integer) in MSVC is extremely slow :eek:

In my quick test, which rounds an array of randomly generated double's to long's, a custom inline ASM implementation of the lrint() function is about 167x faster than the built-in one!

static __inline long int ASM_lrint(double flt)
{
int intgr;
_controlfp(_RC_NEAR, _MCW_RC);
_asm
{
fld flt
fistp intgr
};
return intgr;
}


Even the following plain C implementation, which I took from Qt library, still runs about 16x faster than the built-in lrint() function:
static __inline long QT_lrint(double d)
{
return d >= double(0.0) ? int(d + double(0.5)) : int(d - int(d - 1) + double(0.5)) + int(d - 1);
}


My test code checks whether all results are the same. And, indeed, they are. So does anybody know what's going on here? Did M$ simply screw up, or am I missing something? :confused:

Groucho2004
3rd February 2017, 22:02
Let the compiler produce assembly output and have a look.

LoRd_MuldeR
3rd February 2017, 22:59
Well, I'm not an expert on x86 assembly, but I can see that the compiler issues a call to _lrint function:
; 58 : for (size_t i = 0; i < COUNT; i++)
; 59 : {
; 60 : lng1[i] = lrint(dbl[i]);

000f0 8b 0d 00 00 00
00 mov ecx, DWORD PTR ?dbl@@3PANA
000f6 83 ec 08 sub esp, 8
000f9 f2 0f 10 04 0e movsd xmm0, QWORD PTR [esi+ecx]
000fe f2 0f 11 04 24 movsd QWORD PTR [esp], xmm0
00103 e8 00 00 00 00 call _lrint
00108 8b 0d 00 00 00
00 mov ecx, DWORD PTR ?lng1@@3PAJA
0010e 8d 7f 04 lea edi, DWORD PTR [edi+4]
00111 83 c6 08 add esi, 8
00114 83 c4 08 add esp, 8
00117 89 44 0f fc mov DWORD PTR [edi+ecx-4], eax
0011b 81 fe f8 b3 c4
04 cmp esi, 79999992 ; 04c4b3f8H
00121 72 cd jb SHORT $LL10@main

; 61 : }

Groucho2004
3rd February 2017, 23:07
Can you also post the code for "_lrint"?

LoRd_MuldeR
3rd February 2017, 23:36
Can you also post the code for "_lrint"?

Nope. At least not easily. That's because it's not generated by the compiler, but called from the existing C-Runtime library (API-MS-WIN-CRT-MATH-L1-1-0.DLL).

I'd need a disassembler to inspect that code, I guess.

Anyhow, the fact that lrint() obviously doesn't get inlined (although function-inlining is set to "any suitable" in project settings) while the alternatives do, explains some slowdown.

But that much? :confused:

Groucho2004
3rd February 2017, 23:55
Nope. At least not easily. That's because it's not generated by the compiler, but called from the existing C-Runtime library (API-MS-WIN-CRT-MATH-L1-1-0.DLL).

I'd need a disassembler to inspect that code, I guess.

Anyhow, the fact that lrint() obviously doesn't get inlined (although function-inlining is set to "any suitable" in project settings) while the alternative do, explains some slowdown.

But that much? :confused:
Don't bother finding out why, just roll your own function. I have been using the C implementation as you posted above for as long as I can remember.

nevcairiel
4th February 2017, 08:05
lrint in MSVC also handles special cases like infinity and NaN, as well as checking for overflowing the return type, which makes it safe, but also slower then any hand-optimized version. Not to mention that calling a function has some overhead an inline function wouldn't have.

LoRd_MuldeR
4th February 2017, 14:12
lrint in MSVC also handles special cases like infinity and NaN, as well as checking for overflowing the return type, which makes it safe, but also slower then any hand-optimized version. Not to mention that calling a function has some overhead an inline function wouldn't have.

Thanks, so that's probably the reason (actual function call + special case checking).

Yesterday I made a mistake by only testing positive doubles. I changed my test code to include positive and negative integers (with probability of 0.5 each) and results look like this now:
Time #0: 0.1419288 (Speedup: 1.0x) <-- built-in lrint() function
Time #1: 0.0082809 (Speedup: 17.1x) <-- custom ASM with explicitly setting rounding mode
Time #2: 0.0010908 (Speedup: 130.1x) <-- custom ASM *without* setting rounding mode
Time #3: 0.0042213 (Speedup: 33.6x) <-- qRound() from Qt library (plain C code)
Time #4: 0.0223389 (Speedup: 6.4x) <-- built-in round() function + truncate to long

Interestingly, results from MinGW/GCC binary of the same code look very different:
Time #0: 0.0022093 (Speedup: 1.0x) <-- built-in lrint() function
Time #3: 0.0172325 (Speedup: 0.1x) <-- qRound() from Qt library (plain C code)
Time #4: 0.0245935 (Speedup: 0.1x) <-- built-in round() function + truncate to long

For some reason, built-in lrint() function in MinGW/GCC is blazing fast, but at the same time it doesn't seem to like qRound() from Qt library at all...

lvqcl
4th February 2017, 17:53
g++ 6.3.0, i686-w64-mingw32:

no options:
Time #0: 0.0067700 (Speedup: 1.0x)
Time #3: 0.0334978 (Speedup: 0.2x)
Time #4: 0.0484172 (Speedup: 0.1x)

-O2:
Time #0: 0.0053520 (Speedup: 1.0x)
Time #3: 0.0136390 (Speedup: 0.4x)
Time #4: 0.0469175 (Speedup: 0.1x)

-O2 -msse2:
Time #0: 0.0054020 (Speedup: 1.0x)
Time #3: 0.0096826 (Speedup: 0.6x)
Time #4: 0.0391896 (Speedup: 0.1x)

Some programs use _mm_cvtsd_si32() instead of calling lrint(), so I also rewrote ASM_lrint2 into
return _mm_cvtsd_si32(_mm_load_sd(&flt));
Result:
-O2 -msse2:
Time #0: 0.0055641 (Speedup: 1.0x)
Time #2: 0.0037371 (Speedup: 1.5x)
Time #3: 0.0096729 (Speedup: 0.6x)
Time #4: 0.0397027 (Speedup: 0.1x)