Каков самый быстрый способ конвертировать float в int на x86?

StackOverflow https://stackoverflow.com/questions/78619

Вопрос

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

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

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

Это было полезно?

Решение

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

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

Другие советы

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

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

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

Самое приятное в этом коде то, что он полностью переносим для всех платформ, соответствующих стандарту IEEE 754. Единственным предположением является то, что для режима округления с плавающей запятой установлено значение «ближайший».Примечание:Портативный в том смысле, что он компилируется и работает.Платформы, отличные от 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;
}

Если вы можете гарантировать, что процессор, на котором работает ваш код, совместим с 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

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

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

В базе кода 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

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

Если у вас 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 на другой режим округления.

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

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

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

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

 #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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top