Всем привет. Собственно вот встала тут задача. Надо реализовать функцию floor_i32() которая на входе принимает float, а на выходе выдаёт округлённое к минус бесконечности целое число (в целочисленном формате). Пока сделал так: Код (Text): int floor_i32(float x) { int i=x; if(i>x) i--; return i; } Но всё приходит к тому с чего началось. А началось всё с того что floor() из RTL сильно медленный, В приведённом выше коде проблема вся в приведении float к int. Компилятор пихает функцию из RTL, а она медленная, т.к. содерхит дофига проверок, изменяет fpu control word плюс результат возвращает в __int64 - а это лишние вычисления. Собственно и от floor() я отказался потому что она возвращает float, и вылазит то же приведение типов. Вот возник вопрос - как сделать быстрое приведение float к int. Теоретически мой сишный код написанный выше будет работать в любом случае независимо от режима округления fpu, т.е. меня устроит приведение float к int простыми fld/fistp, но как это компилятору объяснить... Встроенный ассемблер не подходит по той причине что компилятор тогда функцию не инлайнит Есть какие-нибудь трюки для приведения float к int, или для округления к минус бесконечности?
А так не покатит: Код (Text): int floor_i32(float x) { void* tmp = (void*)&x; //возможно к void* и приводить не надо int* tmp2 = (int*)tmp; //нужно потестить int tmp3 = *tmp2; //округляем руками tmp3, как нам надо return tmp3; } Пробовал, компилятор инлайнит и никаких преобразований не делает.
Это то же самое что и i32=*(int *)&f32; Только ты получиш не округлённое значение. Мне для 1.7 надо получить 1, а твой код получит 3F800000h. Парсить руками точно быстрее не будет.
в С99 есть функции Код (Text): long int lrint (double x) ; long int lrintf (float x) ; http://mega-nerd.com/FPcast/ но в MSVS их нету.
Для чисел double: Код (Text): volatile double magic; // вычисление magic _control87( _PC_64 | _RC_DOWN, _MCW_PC | _MCW_RC ); volatile double x; magic = 1.0; while ( true ) { x = magic + 0.5; if ( x == magic ) break; magic *= 2.0; } _control87( cw, 0xfffff ); Собственно сам floor(): Код (Text): // на входе - _val типа double static volatile double x; x = _val + magic; // Теперь *(int*)( &x ) == floor( _val ) return *(int*)( &x );
Y_Mur Это не шутка, а реальный код, используемый в одном моем проекте. Идея взята из книги Уоррена "Алгоритмические трюки для программистов". Использует особенности бинарного представления вещественных чисел.
Так как при сложении достаточно большого числа с маленьким произойдет округление, равенство будет выполнено. Хмм... А что, одно сообщение к другому автоматически не подцепляется?
nobodyzzz C99 не подходит. Поищу исходники реализации. Atlantic Я так понимаю что твой код это более общий вид следующего: Код (Text): int ifloor(float sf) { int i32; double df=6755399441055744.0; // 2^51+2^52 df+=sf; i32=*(int *)&df; if(i32 > sf) i32--; return i32; } Здесь if(i32 > sf) i32--; стоит для того чтобы не изменять FPU Control Word - это очень длительная операция которая к тому же сбрасывает конвейер. И это в принципе работает. Я только понять не могу откуда берётся эта константа, и почему при её прибавлении в мантиссе оказывается округлённое число. Может кто-нибудь объяснить? Взято вот отсюда: http://www.df.lth.se/~john_e/gems/gem0042.html PS: да, кстати, а где про это у Уоррена написано (в какой главе)? я там первым делом смотрел - но безуспешно...
cppasm Первая единица в этой константе нужна чтобы гарантировать что порядок числа будет таким, чтобы бит с весом 2^0 находился в младшем бите мантисты. А вторая единица чтобы если число отрицательное, то не испортилась первая единица
Вроди понял. Сочинил такое: Код (Text): int __inline f2int(float f32) { int i32; float tmp; tmp = f32+12582912.0f; // 2^22+2^23 i32 = *(int *)&tmp & 0x3FFFFF; i32 <<= 10; i32 >>= 10; if(i32 > f32) i32--; return i32; } Диапазон значений -(2^21) ... (2^21)-1 Меня устраивает. Если надо 32-битный результат - можно tmp сделать double и константу поменять. Но вот возникли вопросы: закреплено ли в стандарте что компилятор Си обязан для вещественных чисел использовать формат IEEE? Для другого формата хранения финт не пройдёт...
cppasm Мне почему-то кажется что код где промежуточная переменная double будет работать быстрее чем код с двумя сдвигами.
Сейчас померяю. С другой стороны Агнер Фог пишет что не рекомендуется смешивать вычисления float с double... Хотя в варианте с float на 2 сдвига и одно И больше.
Померял. Вариант с double быстрее. Ещё чуть-чуть быстрее такой вариант: Код (Text): int f2int_f64r(float f32) { int i32; double f64,magic=6755399441055744.0; // 2^51+2^52 f64 = f32+magic; i32 = *(int *)&f64; f64 -= magic; if(f64 > f32) i32--; return i32; } Но тут для нормальной работы требуется чтобы разрядность мантиссы была 53бита (это по умолчанию так, но можно изменить на 64). Тогда прибавление 2^51+2^52 отсекает всю дробную часть. Соответственно потом при вычитании этой же константы получаем исходное число, но без дробной части (округлённое). Есть у кого ещё какие идеи как ускорить можно?
У меня есть такая версия: Код (Text): int ifl(float f32) { if(-1.0<f32&&f32<0.0)//if(0xBF800000>*(int*)&f32&&*(int*)&f32>0x80000000) return -1; return (int)(f32+0x40000000)-0x40000000; } работает немного быстрее варианта с double, наверно потому что для приведения к int MSVS2005 использует _ftol2_sse, который использует cvttsd2si. Можно заменить условие тем что в комментариях, вроде еще быстрее работает, только это уже хак. [add]посмотрел что за код со вторым вариантом генерится: Код (Text): 004011F4 mov edx,dword ptr [esp+30h] 004011F8 add edx,7FFFFFFFh 004011FE cmp edx,3F7FFFFEh 00401204 ja main+20Bh (40120Bh) 00401206 or eax,0FFFFFFFFh 00401209 jmp main+21Bh (40121Bh) 0040120B fld dword ptr [esp+30h] 0040120F fadd st,st(1) 00401211 call _ftol2_sse (401970h) 00401216 sub eax,40000000h 0040121B впечатляет
У м$явок есть идеи. в msvcrt Код (Text): oword_7C0412B0 xmmword 3FF00000000000003FF0000000000000h oword_7C0412C0 xmmword 4330000000000000433h oword_7C0412D0 xmmword 4330000000000000BFF0000000000000h oword_7C0412E0 xmmword 80000000000000008000000000000000h oword_7C0412F0 xmmword 7FFh public floor floor proc near var_10 = dword ptr -10h var_C = dword ptr -0Ch var_8 = dword ptr -8 var_4 = dword ptr -4 arg_0 = qword ptr 4 cmp dword_7C04B14C, 0 jz sub_7C0363B7 sub esp, 8 stmxcsr [esp+8+var_4] mov eax, [esp+8+var_4] and eax, 1F80h cmp eax, 1F80h jnz short loc_7C00990F fnstcw word ptr [esp+8+var_8] mov ax, word ptr [esp+8+var_8] and ax, 7Fh cmp ax, 7Fh loc_7C00990F: ; CODE XREF: lea esp, [esp+8] jnz sub_7C0363B7 movq xmm0, [esp+arg_0] movapd xmm2, oword ptr ds:oword_7C0412C0 movapd xmm1, xmm0 movapd xmm7, xmm0 psrlq xmm0, 34h movd eax, xmm0 andpd xmm0, oword ptr ds:oword_7C0412F0 psubd xmm2, xmm0 psrlq xmm1, xmm2 test eax, 800h ...и тому подобный мрак floor endp вероятно нечто очень быстрое, что лишний CALL не жаль потратить. да уж.. зы: хм... cppasm а вообще - я за вариант с float. ибо Intel C++ v8.0 (c опциями /O3 /Ot /G6 /arch:SSE) выдал вот такую фкусняшку под трете-пень. Код (Text): PUBLIC _f2int _f2int PROC NEAR ; parameter 1: 8 + esp .B1.1: ; Preds .B1.0 push esi movss xmm4, DWORD PTR [esp+8] movss xmm3, DWORD PTR _2il0floatpacket.1 addss xmm3, xmm4 movss DWORD PTR [esp], xmm3 mov eax, DWORD PTR [esp] and eax, 4194303 shl eax, 10 sar eax, 10 lea edx, DWORD PTR [eax-1] cvtsi2ss xmm5, eax comiss xmm4, xmm5 cmovb eax, edx pop ecx ret ALIGN 4 _f2int ENDP А на double он таково не выдает :'( (ток инструкшены под FPU ) ззы: и такаяже картина c моим любимым GCC 3.4.6 Код (Text): .def _f2int; .scl 2; _f2int: push ecx fld DWORD PTR [esp+8] fld st(0) fadd DWORD PTR LC0 fstp DWORD PTR [esp] mov eax, DWORD PTR [esp] sal eax, 10 sar eax, 10 cvtsi2ss xmm0, eax lea edx, [eax-1] movss DWORD PTR [esp], xmm0 fld DWORD PTR [esp] fucomip st, st(1) fstp st(0) cmova eax, edx pop edx ret
Я проверял на Intel C++ 10 Разница между вариантами с float и double в пару тактов (1-2), проверял на 100000000 итерациях. При включённом SSE вариант с float незначительно выигрывает. Тут вся фигня в том, что при включённом SSE значительно (тактов 10-15) выигрывает обычное приведение к int. Проблема вся в том что без SSE этот вариант отстаёт очень значительно. На msvc 2005 и 2008 как с SSE, SSE2, так и без выигрывает вариант с double. Вот и сиди выбирай :/ С вариантом Black_mirror сейчас буду разбираться... PS: предпочтение отдаётся FPU, если код будет работать быстрее и на реализации с SSE - это просто ещё один плюс в его пользу, но не основной. PPS: кстати вот замеры на 100000000 итераций опции компилятора: icl /O3 /Ot /G6 /arch:SSE round.c Код (Text): Execution time (float) -> 2103813975 clocks Execution time (double) -> 2316319823 clocks Execution time (c-cast) -> 1601596140 clocks Не такое уж и огромное преимущество.