SSE округляет в меньшую сторону, когда следует округлять в большую сторону

Я работаю над приложением, которое преобразует выборки с плавающей запятой в диапазоне от -1,0 до 1,0 в знаковые 16-битные, чтобы обеспечить точность вывода оптимизированных (SSE) подпрограмм. Я написал набор тестов, которые запускают неоптимизированную версию против версии SSE и сравнивает их вывод.

Прежде чем я начну, я убедился, что режим округления SSE установлен на ближайший.

В моем тестовом примере формула:

ratio = 65536 / 2
output = round(input * ratio)

По большей части результаты точны, но на одном конкретном входе я вижу ошибку для ввода -0.8499908447265625.

-0.8499908447265625 * (65536 / 2) = -27852.5

Обычный код правильно округляет это значение до -27853, но код SSE округляет его до -27852.

Вот используемый код SSE:

void Float_S16(const float *in, int16_t *out, const unsigned int samples)
{
  static float ratio = 65536.0f / 2.0f;
  static __m128 mul  = _mm_set_ps1(ratio);

  for(unsigned int i = 0; i < samples; i += 4, in += 4, out += 4)
  {
    __m128  xin;
    __m128i con;

    xin = _mm_load_ps(in);
    xin = _mm_mul_ps(xin, mul);
    con = _mm_cvtps_epi32(xin);

    out[0] = _mm_extract_epi16(con, 0);
    out[1] = _mm_extract_epi16(con, 2);
    out[2] = _mm_extract_epi16(con, 4);
    out[3] = _mm_extract_epi16(con, 6);
  }
}

Автономный пример по запросу:

/* standard math */
float   ratio  = 65536.0f / 2.0f;
float   in [4] = {-1.0, -0.8499908447265625, 0.0, 1.0};
int16_t out[4];
for(int i = 0; i < 4; ++i)
  out[i] = round(in[i] * ratio);

/* sse math */
static __m128 mul  = _mm_set_ps1(ratio);
__m128  xin;
__m128i con;

xin = _mm_load_ps(in);
xin = _mm_mul_ps(xin, mul);
con = _mm_cvtps_epi32(xin);

int16_t outSSE[4];
outSSE[0] = _mm_extract_epi16(con, 0);
outSSE[1] = _mm_extract_epi16(con, 2);
outSSE[2] = _mm_extract_epi16(con, 4);
outSSE[3] = _mm_extract_epi16(con, 6);

printf("Standard = %d, SSE = %d\n", out[1], outSSE[1]);

person Geoffrey    schedule 14.10.2015    source источник
comment
Можете ли вы сократить это до самостоятельной программы-примера, демонстрирующей проблему?   -  person Jason R    schedule 14.10.2015
comment
Может быть полезно сохранить значения аргументов до и после выполнения.   -  person CinchBlue    schedule 14.10.2015
comment
Это поведение по умолчанию для всех операций с плавающей запятой, а не только для SSE. Округление от половины до четного или банковского округления — это режим округления по умолчанию в соответствии со стандартом IEEE 754. . Причина в том, что это сводит к минимуму ошибки округления при применении ко многим числам, в то время как округление гарантирует наличие ошибки в полпункта.   -  person Panagiotis Kanavos    schedule 14.10.2015


Ответы (2)


Хотя режим округления SSE по умолчанию «округление до ближайшего», это не старый знакомый метод округления, который мы все изучали в школе, а немного более современный вариант, известный как Округление банкира (также известное как беспристрастное округление, конвергентное округление, статистическое округление, округление по голландскому методу, округление по Гауссу или округление от нечетного до четного), которое округляет до ближайшего четного целого числа. Этот метод округления предположительно лучше, чем более традиционный метод, со статистической точки зрения. Вы увидите такое же поведение с такими функциями, как rint(), а также режим округления по умолчанию для IEEE-754.

Также обратите внимание, что в то время как функция стандартной библиотеки round() использует традиционный метод округления, инструкция SSE ROUNDPS (_mm_round_ps) использует банковское округление.

person Paul R    schedule 14.10.2015
comment
Следует отметить, что банковское округление используется по умолчанию для любой обработки с плавающей запятой, а не только для SSE. - person Panagiotis Kanavos; 14.10.2015
comment
@PanagiotisKanavos: спасибо - я как раз добавлял примечание о методе округления по умолчанию для IEEE-754. - person Paul R; 14.10.2015
comment
Можно ли как-то настроить округление традиционным способом в SSE/AVX? - person TStancek; 29.05.2018

Это поведение по умолчанию для всех операций с плавающей запятой, а не только для SSE. Округление от половины до четного или банковское округление — это режим округления по умолчанию в соответствии со стандартом IEEE 754.

Причина, по которой это используется, заключается в том, что постоянное округление в большую (или меньшую) сторону приводит к ошибке в полбалла, которая накапливается даже при небольшом числе операций. Половина очков может привести к довольно значительным ошибкам — настолько значительным, что они стали сюжетной точкой в ​​​​Супермене 3.

Однако округление половины до четного или нечетного приводит как к отрицательным, так и к положительным ошибкам половины точки, которые устраняют друг друга при применении ко многим операциям.

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

person Panagiotis Kanavos    schedule 14.10.2015
comment
Я думаю, реальный пример, а не фильм о Супермене более интересен. - person Z boson; 19.10.2015
comment
Из ссылки на вики. Известный случай связан с новым индексом, созданным Ванкуверской фондовой биржей в 1982 году. Первоначально он был установлен на уровне 1000 000 (точность до трех знаков после запятой), а через 22 месяца упал примерно до 520, тогда как цены на акции в целом выросли за этот период. . Проблема была вызвана тем, что индекс пересчитывался тысячи раз в день и всегда округлялся до 3 знаков после запятой таким образом, что накапливались ошибки округления. Пересчет с лучшим округлением дал значение индекса 1098,892 на конец того же периода. - person Z boson; 19.10.2015