Какой самый быстрый способ конвертировать float в int на x86 - PullRequest
22 голосов
/ 17 сентября 2008

Какой самый быстрый из известных вам способов преобразования чисел с плавающей запятой в тип int на процессоре x86. Предпочтительно в C или сборке (которая может быть встроена в C) для любой комбинации следующего:

  • 32/64/80-битное число с плавающей запятой -> 32/64-битное целое число

Я ищу способ, который быстрее, чем просто позволить компилятору это сделать.

Ответы [ 10 ]

17 голосов
/ 17 сентября 2008

Это зависит от того, хотите ли вы усеченное преобразование или округление и с какой точностью. По умолчанию C будет выполнять усеченное преобразование при переходе от float к int. Существуют инструкции FPU, которые делают это, но это не преобразование ANSI C, и существуют значительные предостережения при его использовании (например, знание состояния округления FPU). Поскольку ответ на вашу проблему довольно сложен и зависит от некоторых переменных, которые вы не выразили, я рекомендую эту статью по этому вопросу:

http://www.stereopsis.com/FPU.html

13 голосов
/ 17 сентября 2008

Упакованное преобразование с использованием SSE является самым быстрым методом, поскольку вы можете преобразовать несколько значений в одной инструкции. ffmpeg имеет много сборок для этого (в основном для преобразования декодированного вывода аудио в целочисленные сэмплы); проверьте это для некоторых примеров.

9 голосов
/ 23 сентября 2008

Обычно используемый трюк для простого кода x86 / x87 состоит в том, чтобы заставить часть мантиссы float представлять int. Следует 32-битная версия.

64-битная версия аналогична. Вышеприведенная версия Lua работает быстрее, но основывается на усечении двойного до 32-битного результата, поэтому требует, чтобы для модуля x87 была установлена ​​двойная точность, и не может быть адаптирована для преобразования двойного в 64-битное int. *

Приятной особенностью этого кода является то, что он полностью переносим для всех платформ, соответствующих стандарту IEEE 754, единственное допущение заключается в том, что режим округления с плавающей запятой установлен на ближайший. Примечание: Portable в том смысле, что он компилируется и работает. Платформы, отличные от x86, обычно не очень выигрывают от этой техники, если вообще ее используют.

static const float Snapper=3<<22;

union UFloatInt {
 int i;
 float f;
};

/** by Vlad Kaipetsky
portable assuming FP24 set to nearest rounding mode
efficient on x86 platform
*/
inline int toInt( float fval )
{
  Assert( fabs(fval)<=0x003fffff ); // only 23 bit values handled
  UFloatInt &fi = *(UFloatInt *)&fval;
  fi.f += Snapper;
  return ( (fi.i)&0x007fffff ) - 0x00400000;
}
7 голосов
/ 15 марта 2009

Если вы можете гарантировать, что процессор, на котором работает ваш код, совместим с SSE3 (даже Pentium 5 - JBB), вы можете разрешить компилятору использовать его инструкцию FISTTP (т.е. -msse3 для gcc). Кажется, он делает то, что всегда должен был делать:

http://software.intel.com/en-us/articles/how-to-implement-the-fisttp-streaming-simd-extensions-3-instruction/

Обратите внимание, что FISTTP отличается от FISTP (у которого есть проблемы, вызывающие медлительность). Он входит в состав SSE3, но на самом деле является (единственным) усовершенствованием на стороне X87.

В остальном, в отличие от процессоров X86, в любом случае, возможно, преобразование будет вполне нормальным. :)

Процессоры с поддержкой SSE3

6 голосов
/ 17 сентября 2008

База кода Lua имеет следующий фрагмент кода для этого (проверьте src / luaconf.h с www.lua.org). Если вы найдете (ТАК находит) более быстрый путь, я уверен, что они будут в восторге.

О, lua_Number означает двойной. :)

/*
@@ lua_number2int is a macro to convert lua_Number to int.
@@ lua_number2integer is a macro to convert lua_Number to lua_Integer.
** CHANGE them if you know a faster way to convert a lua_Number to
** int (with any rounding method and without throwing errors) in your
** system. In Pentium machines, a naive typecast from double to int
** in C is extremely slow, so any alternative is worth trying.
*/

/* On a Pentium, resort to a trick */
#if defined(LUA_NUMBER_DOUBLE) && !defined(LUA_ANSI) && !defined(__SSE2__) && \
    (defined(__i386) || defined (_M_IX86) || defined(__i386__))

/* On a Microsoft compiler, use assembler */
#if defined(_MSC_VER)

#define lua_number2int(i,d)   __asm fld d   __asm fistp i
#define lua_number2integer(i,n)     lua_number2int(i, n)

/* the next trick should work on any Pentium, but sometimes clashes
   with a DirectX idiosyncrasy */
#else

union luai_Cast { double l_d; long l_l; };
#define lua_number2int(i,d) \
  { volatile union luai_Cast u; u.l_d = (d) + 6755399441055744.0; (i) = u.l_l; }
#define lua_number2integer(i,n)     lua_number2int(i, n)

#endif

/* this option always works, but may be slow */
#else
#define lua_number2int(i,d) ((i)=(int)(d))
#define lua_number2integer(i,d) ((i)=(lua_Integer)(d))

#endif
6 голосов
/ 17 сентября 2008

Существует одна инструкция для преобразования числа с плавающей точкой в ​​сборку int: используйте инструкцию FISTP. Он извлекает значение из стека с плавающей запятой, преобразует его в целое число и затем сохраняет по указанному адресу. Я не думаю, что был бы более быстрый путь (если вы не используете расширенные наборы инструкций, такие как MMX или SSE, с которыми я не знаком).

Другая инструкция, FIST, оставляет значение в стеке FP, но я не уверен, что он работает с адресатами размером в четыре слова.

4 голосов
/ 26 февраля 2014

Я предполагаю, что требуется усечение, так же, как если бы вы написали i = (int)f в "C".

Если у вас SSE3, вы можете использовать:

int convert(float x)
{
    int n;
    __asm {
        fld x
        fisttp n // the extra 't' means truncate
    }
    return n;
}

Альтернативно, с SSE2 (или в x64, где встроенная сборка может быть недоступна), вы можете использовать почти так же быстро:

#include <xmmintrin.h>
int convert(float x)
{
    return _mm_cvtt_ss2si(_mm_load_ss(&x)); // extra 't' means truncate
}

На старых компьютерах есть возможность установить режим округления вручную и выполнить преобразование с помощью обычной инструкции fistp. Это, вероятно, будет работать только для массивов с плавающей запятой, в противном случае следует позаботиться о том, чтобы не использовать какие-либо конструкции, которые заставили бы компилятор изменить режим округления (например, приведение). Это делается так:

void Set_Trunc()
{
    // cw is a 16-bit register [_ _ _ ic rc1 rc0 pc1 pc0 iem _ pm um om zm dm im]
    __asm {
        push ax // use stack to store the control word
        fnstcw word ptr [esp]
        fwait // needed to make sure the control word is there
        mov ax, word ptr [esp] // or pop ax ...
        or ax, 0xc00 // set both rc bits (alternately "or ah, 0xc")
        mov word ptr [esp], ax // ... and push ax
        fldcw word ptr [esp]
        pop ax
    }
}

void convertArray(int *dest, const float *src, int n)
{
    Set_Trunc();
    __asm {
        mov eax, src
        mov edx, dest
        mov ecx, n // load loop variables

        cmp ecx, 0
        je bottom // handle zero-length arrays

    top:
        fld dword ptr [eax]
        fistp dword ptr [edx]
        loop top // decrement ecx, jump to top
    bottom:
    }
}

Обратите внимание, что встроенная сборка работает только с компиляторами Microsoft Visual Studio (и, возможно, Borland), ее необходимо переписать в сборку GNU для компиляции с gcc. Однако решение SSE2 со встроенными функциями должно быть достаточно переносимым.

Другие режимы округления возможны с помощью других встроенных функций SSE2 или путем ручной установки управляющего слова FPU в другой режим округления.

3 голосов
/ 01 июня 2013

Поскольку MS выводит нас из встроенной сборки в X64 и вынуждает нас использовать встроенные функции, я искал, что использовать. MSDN doc дает _mm_cvtsd_si64x с примером.

Пример работает, но ужасно неэффективен, используя невыровненную нагрузку в два раза, где нам нужна только одна нагрузка, поэтому избавляемся от дополнительных требований выравнивания. Затем производится много ненужных нагрузок и перезагрузок, но их можно устранить следующим образом:

 #include <intrin.h>
 #pragma intrinsic(_mm_cvtsd_si64x)
 long long _inline double2int(const double &d)
 {
     return _mm_cvtsd_si64x(*(__m128d*)&d);
 }

Результат:

        i=double2int(d);
000000013F651085  cvtsd2si    rax,mmword ptr [rsp+38h]  
000000013F65108C  mov         qword ptr [rsp+28h],rax  

Режим округления можно установить без встроенной сборки, например,

    _control87(_RC_NEAR,_MCW_RC);

где округление до ближайшего по умолчанию (в любом случае).

На вопрос, устанавливать ли режим округления при каждом вызове или предполагать, что он будет восстановлен (сторонние библиотеки), я должен ответить по опыту, я полагаю. Вам нужно будет включить float.h для _control87() и связанных с ним констант.

И, нет, это не будет работать в 32 битах, поэтому продолжайте использовать инструкцию FISTP:

_asm fld d
_asm fistp i
3 голосов
/ 17 сентября 2008

Если вы действительно заботитесь о скорости, убедитесь, что ваш компилятор генерирует инструкцию FIST. В MSVC вы можете сделать это с помощью / QIfist, см. Этот обзор MSDN

Вы также можете рассмотреть возможность использования встроенных функций SSE, см. Эту статью Intel: http://softwarecommunity.intel.com/articles/eng/2076.htm

0 голосов
/ 17 сентября 2008

Как правило, вы можете доверять компилятору, чтобы он был эффективным и правильным. Обычно ничего не получится, если свести собственные функции к чему-то, что уже существует в компиляторе.

...