Как убыстриться с плавающей точкой к преобразованию целого числа? [дубликат]

19
задан Mysticial 15 December 2014 в 07:44
поделиться

14 ответов

Большинство других ответов здесь просто пытается устранить цикл наверху.

Только ответ deft_code добирается до сути того, что вероятно настоящая проблема - что преобразование плавающей точки к целым числам является очень дорогим на x86 процессоре. решение deft_code правильно, хотя он не дает цитаты или объяснения.

Вот источник приема с некоторым объяснением и также версиями, характерными для того, хотите ли Вы окружить, вниз, или к нулю: Знают, что Ваш FPU

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

15
ответ дан 30 November 2019 в 02:48
поделиться

Если Вы имеете очень большие массивы (больше, чем некоторые МБ - размер кэша ЦП), время Ваш код и видите, какова пропускная способность. Вы, вероятно, насыщаете шину памяти, не единицу FP. Ищите максимальную теоретическую пропускную способность для своего ЦП и посмотрите, как близко к нему Вы.

, Если Вы ограничиваетесь шиной памяти, дополнительные потоки просто сделают ее хуже. Вам нужны лучшие аппаратные средства (например, более быстрая память, другой ЦП, другая материнская плата).

<час> В ответ на комментарий Larry Gritz...

Вы корректны: FPU является главным узким местом (и использование приема xs_CRoundToInt позволяет очень близко подходить к насыщению шины памяти).

Вот некоторые результаты испытаний для процессора (Q6600) Core 2. Теоретическая пропускная способность оперативной памяти для этой машины составляет 3,2 ГБ/с (L1, и пропускная способность L2 намного выше). Код был скомпилирован с Visual Studio 2008. Подобные результаты для 32-разрядного и 64-разрядного, и с/O2 или оптимизацией Вола/.

WRITING ONLY...
  1866359 ticks with 33554432 array elements (33554432 touched).  Bandwidth: 1.91793 GB/s
  154749 ticks with 262144 array elements (33554432 touched).  Bandwidth: 23.1313 GB/s
  108816 ticks with 8192 array elements (33554432 touched).  Bandwidth: 32.8954 GB/s

USING CASTING...
  5236122 ticks with 33554432 array elements (33554432 touched).  Bandwidth: 0.683625 GB/s
  2014309 ticks with 262144 array elements (33554432 touched).  Bandwidth: 1.77706 GB/s
  1967345 ticks with 8192 array elements (33554432 touched).  Bandwidth: 1.81948 GB/s

USING xs_CRoundToInt...
  1490583 ticks with 33554432 array elements (33554432 touched).  Bandwidth: 2.40144 GB/s
  1079530 ticks with 262144 array elements (33554432 touched).  Bandwidth: 3.31584 GB/s
  1008407 ticks with 8192 array elements (33554432 touched).  Bandwidth: 3.5497 GB/s

(Windows) исходный код:

// floatToIntTime.cpp : Defines the entry point for the console application.
//

#include <windows.h>
#include <iostream>

using namespace std;

double const _xs_doublemagic = double(6755399441055744.0);
inline int xs_CRoundToInt(double val, double dmr=_xs_doublemagic) { 
  val = val + dmr; 
  return ((int*)&val)[0]; 
} 

static size_t const N = 256*1024*1024/sizeof(double);
int    I[N];
double F[N];
static size_t const L1CACHE = 128*1024/sizeof(double);
static size_t const L2CACHE = 4*1024*1024/sizeof(double);

static size_t const Sz[]    = {N,     L2CACHE/2,     L1CACHE/2};
static size_t const NIter[] = {1, N/(L2CACHE/2), N/(L1CACHE/2)};

int main(int argc, char *argv[])
{
  __int64 freq;
  QueryPerformanceFrequency((LARGE_INTEGER*)&freq);

  cout << "WRITING ONLY..." << endl;
  for (int t=0; t<3; t++) {
    __int64 t0,t1;
    QueryPerformanceCounter((LARGE_INTEGER*)&t0);
    size_t const niter = NIter[t];
    size_t const sz    = Sz[t];
    for (size_t i=0; i<niter; i++) {
      for (size_t n=0; n<sz; n++) {
        I[n] = 13;
      }
    }
    QueryPerformanceCounter((LARGE_INTEGER*)&t1);
    double bandwidth = 8*niter*sz / (((double)(t1-t0))/freq) / 1024/1024/1024;
    cout << "  " << (t1-t0) << " ticks with " << sz 
         << " array elements (" << niter*sz << " touched).  " 
         << "Bandwidth: " << bandwidth << " GB/s" << endl;
  }

  cout << "USING CASTING..." << endl;
  for (int t=0; t<3; t++) {
    __int64 t0,t1;
    QueryPerformanceCounter((LARGE_INTEGER*)&t0);
    size_t const niter = NIter[t];
    size_t const sz    = Sz[t];
    for (size_t i=0; i<niter; i++) {
      for (size_t n=0; n<sz; n++) {
        I[n] = (int)F[n];
      }
    }
    QueryPerformanceCounter((LARGE_INTEGER*)&t1);
    double bandwidth = 8*niter*sz / (((double)(t1-t0))/freq) / 1024/1024/1024;
    cout << "  " << (t1-t0) << " ticks with " << sz 
         << " array elements (" << niter*sz << " touched).  " 
         << "Bandwidth: " << bandwidth << " GB/s" << endl;
  }

  cout << "USING xs_CRoundToInt..." << endl;
  for (int t=0; t<3; t++) {
    __int64 t0,t1;
    QueryPerformanceCounter((LARGE_INTEGER*)&t0);
    size_t const niter = NIter[t];
    size_t const sz    = Sz[t];
    for (size_t i=0; i<niter; i++) {
      for (size_t n=0; n<sz; n++) {
        I[n] = xs_CRoundToInt(F[n]);
      }
    }
    QueryPerformanceCounter((LARGE_INTEGER*)&t1);
    double bandwidth = 8*niter*sz / (((double)(t1-t0))/freq) / 1024/1024/1024;
    cout << "  " << (t1-t0) << " ticks with " << sz 
         << " array elements (" << niter*sz << " touched).  " 
         << "Bandwidth: " << bandwidth << " GB/s" << endl;
  }

  return 0;
}
-1
ответ дан 30 November 2019 в 02:48
поделиться

Я удивлен Вашим результатом. Какой компилятор Вы используете? Вы компилируете с оптимизацией, превращенной полностью? Вы подтвердили использование valgrind и Kcachegrind, который это - то, где узкое место? Какой процессор Вы используете? На что похож ассемблерный код?

само преобразование должно быть скомпилировано в единственную инструкцию. Хороший оптимизирующий компилятор должен развернуть цикл так, чтобы несколько преобразований были сделаны на тест-и-ответвление. Если этого не происходит, Вы можете разворачивать цикл рукой :

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];

, Если Ваш компилятор действительно вызывает жалость, Вы, возможно, должны были бы помочь ему с общими подвыражениями, например,

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];

Действительно сообщают с большим количеством информации!

0
ответ дан 30 November 2019 в 02:48
поделиться

На Intel Ваш лучший выбор является встроенными вызовами SSE2.

0
ответ дан 30 November 2019 в 02:48
поделиться

большинство компиляторов C генерирует вызовы к _ftol или чему-то для каждого плавания к международному преобразованию. помещение уменьшенного переключателя соответствия с плавающей точкой (как fp:fast) могло бы помочь - ЕСЛИ Вы понимаете И принимаете другие эффекты этого переключателя. кроме этого, помещает вещь в трудный блок или sse внутренний цикл, ЕСЛИ Вы в порядке И понимаете другое поведение округления. для больших циклов как Ваш пример необходимо записать функцию, которая настраивает управляющие слова с плавающей точкой однажды и затем делает объем, округляющийся с только fistp инструкции, и затем сбрасывает управляющее слово - ЕСЛИ Вы соглашаетесь с x86 только путь выполнения кода, но по крайней мере Вы не измените округление. прочитайте на fld и fistp fpu инструкции и fpu управляющее слово.

1
ответ дан 30 November 2019 в 02:48
поделиться

См. эту статью Intel для ускорения целочисленных преобразований:

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

По данным Microsoft,/QIfist параметр компилятора удерживается от использования в VS 2005, потому что целочисленное преобразование было ускорено. Они забыли говорить, как это было ускорено, но рассмотрение списка дизассемблирования могло бы дать ключ к разгадке.

http://msdn.microsoft.com/en-us/library/z8dh4h17 (по сравнению с 80) .aspx

1
ответ дан 30 November 2019 в 02:48
поделиться

Какой компилятор Вы используете? В более свежих компиляторах C/C++ Microsoft существует опция под C/C++-> Генерация кода-> модель С плавающей точкой, которая имеет опции: быстрый, точный, строгий. Я думаю точный, значение по умолчанию и работает путем эмуляции операций FP в некоторой степени. Если Вы используете компилятор MS, как эта опция установлена? Это помогает установить его на "быстро"? В любом случае, на что похоже дизассемблирование?

, Поскольку thirtyseven сказал выше, ЦП может преобразовать float<->int в по существу одной инструкции, и это не добирается немного быстрее, чем тот (за исключением операции SIMD).

Также примечание, что современные центральные процессоры используют ту же единицу FP и для единственного (32 бита) и для двойные числа FP (на 64 бита), поэтому если Вы не пытаетесь сохранить память, хранящую много плаваний, нет действительно никакой причины способствовать float по дважды.

0
ответ дан 30 November 2019 в 02:48
поделиться

Вы смогли загружать все целые числа в модуль SSE Вашего процессора с помощью некоторого волшебного ассемблерного кода, затем сделайте эквивалентный код для устанавливания значений к ints, затем считайте их как плавания. Я не уверен, что это было бы немного быстрее все же. Я не гуру SSE, таким образом, я не знаю, как сделать это. Возможно, кто-то еще может вмешаться.

1
ответ дан 30 November 2019 в 02:48
поделиться

Ключ должен избежать _ftol () функция, которая является напрасно медленной. Ваш лучший выбор для длинных списков данных как это состоит в том, чтобы использовать инструкцию SSE2 cvtps2dq для преобразования двух упакованных плаваний в два, упаковал int64s. Сделайте это дважды (получение четырех int64s через два регистра SSE), и можно переставить их вместе для получения четырех int32s (теряющий лучшие 32 бита каждого результата преобразования). Вам не нужен блок, чтобы сделать это; MSVC выставляет компилятор intrinsics соответствующим инструкциям - _mm_cvtpd_epi32 () , если моя память служит мне правильно.

, Если Вы делаете это, очень важно, чтобы Ваше плавание и интервал выстроили быть 16 байтов, выровненных так, чтобы загрузка и хранение SSE2 intrinsics могла работать с максимальной производительностью. Кроме того, я рекомендую Вам конвейер программного обеспечения немного и обрабатываю шестнадцать плавания сразу в каждом цикле, например, (предполагающий, что "функции" здесь являются на самом деле вызовами к компилятору 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);
}

причина этого состоит в том, что инструкции SSE имеют длинную задержку, поэтому если Вы будете следовать за загрузкой в xmm0 сразу с зависимой операцией на xmm0 затем, то у Вас будет останов. Наличие нескольких регистров "в полете" сразу скрывает задержку немного. (Теоретически волшебный всезнающий компилятор мог исказить свой путь вокруг этой проблемы, но на практике это не делает.)

В случае неудачи амулет SSE можно предоставить/QIfist опцию к MSVC, который заставит это выпускать единственный кулак кода операции вместо вызова к _ftol; это означает, что будет просто использовать, какой бы ни округление режима, оказывается, установлено в ЦП, не удостоверяясь, что это - определенный усеченный op C ANSI. В документах Microsoft говорится, что/QIfist удерживается от использования, потому что их код с плавающей точкой быстр теперь, но дизассемблер покажет Вам, что это невыровнено оптимистично. Даже/fp:fast просто заканчивается к вызову к _ftol_sse2, который, хотя быстрее, чем вопиющий _ftol все еще вызов функции, сопровождаемый скрытым SSE op, и таким образом излишне замедляется.

я предполагаю, что Вы находитесь на x86 дуге, между прочим - если Вы находитесь на PPC существуют эквивалентные операции VMX, или можно использовать прием magic-number-multiply, упомянутый выше сопровождаемый vsel (для каширования битов немантиссы) и выровненное хранилище.

2
ответ дан 30 November 2019 в 02:48
поделиться

Действительно ли время является достаточно большим, что оно перевешивает стоимость запуска нескольких потоков?

Принятие у Вас есть многоядерный процессор или несколько процессоров на Вашем поле, которое Вы могли использовать в своих интересах, это будет тривиальной задачей параллелизировать через несколько потоков.

2
ответ дан 30 November 2019 в 02:48
поделиться

Существует инструкция FISTTP в системе команд SSE3, которая делает то, что Вы хотите, но относительно того, могла ли она быть использована и привести к более быстрым результатам, чем libc, я понятия не имею.

3
ответ дан 30 November 2019 в 02:48
поделиться
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]);

двойной параметр не является ошибкой! Существует способ сделать этот прием с плаваниями непосредственно, но это получает ужасную попытку покрыть все угловые случаи. В его текущей форме эта функция будет вокруг плавания ближайшее целое число, если Вы захотите усечение, вместо этого используют 6755399441055743.5 (0.5 меньше).

14
ответ дан 30 November 2019 в 02:48
поделиться

я запустил некоторые тесты на различных способах сделать преобразование плавания к интервалу. Короткий ответ должен предположить, что Ваш клиент имеет SSE2-способные центральные процессоры и установил/arch:SSE2 флаг компилятора. Это позволит компилятору использовать скаляр SSE инструкции, которые являются дважды с такой скоростью, как даже метод магического числа.

Иначе, если у Вас есть длинные строки плаваний для шлифования, используют упакованную операцию в секунду SSE2.

8
ответ дан 30 November 2019 в 02:48
поделиться

В Visual C++ 2008 компилятор генерирует вызовы SSE2 отдельно, если Вы делаете сборку конечных версий с истраченными опциями оптимизации и смотрите на дизассемблирование (хотя некоторые условия нужно соблюдать, игра вокруг с Вашим кодом).

1
ответ дан 30 November 2019 в 02:48
поделиться