В чем разница в производительности между целыми числами без знака и со знаком? [закрыто]

42

Мне известно о падении производительности при смешивании подписанных целых с плавающей точкой.

Хуже ли смешивать неподписанные целые с поплавками?

Есть ли хит при смешивании подписанного / неподписанного без поплавков?

Влияют ли разные размеры (u32, u16, u8, i32, i16, i8) на производительность? На каких платформах?

Луис
источник
2
Я удалил PS3-специфичный текст / тег, потому что это хороший вопрос для любой архитектуры, и ответ остается верным для всех архитектур, которые разделяют целочисленные регистры и регистры с плавающей запятой, а это практически все из них.

Ответы:

36

Большой штраф от смешивания целых чисел (любого типа) и чисел с плавающей точкой заключается в том, что они находятся в разных наборах регистров. Чтобы перейти от одного набора регистров к другому, вы должны записать значение в память и прочитать его обратно, что приводит к остановке load-hit-store .

При переходе между разными размерами или числом знаков со знаком все остается в одном наборе регистров, поэтому вы избежите большого штрафа. Из-за расширения знака и т. Д. Могут быть меньшие штрафы, но они намного меньше, чем в хранилище при загрузке.

celion
источник
В статье, на которую вы ссылаетесь, говорится, что процессор ячейки PS3 является исключением из этого, потому что, очевидно, все хранится в одном и том же наборе регистров (можно найти примерно в середине статьи или выполнить поиск по «ячейке»).
Bummzack
4
@bummzack: Это относится только к SPE, а не к СИЗ; SPE имеют очень особенную среду с плавающей точкой, и приведение все еще относительно дорого. Кроме того, затраты остаются одинаковыми для целых чисел со знаком и без знака.
Это хорошая статья, и важно знать о LHS (и я голосую за это), но мой вопрос касается этих штрафов, связанных со знаками. Я знаю, что они небольшие и, вероятно, незначительные, но я все же хотел бы увидеть некоторые реальные цифры или ссылки на них.
Луис
1
@Luis - я пытался найти какую-то общедоступную документацию по этому вопросу, но пока не могу ее найти. Если у вас есть доступ к документации по Xbox360, есть хороший технический документ Брюса Доусона, который охватывает некоторые из них (и в целом он очень хорош).
Целион
@Luis: Я разместил анализ ниже, но если он вас устраивает, пожалуйста, дайте ответ Целиону - все, что он сказал, правильно, все, что я сделал, это запускал GCC несколько раз.
12

Я подозреваю, что информация о Xbox 360 и PS3 определенно будет находиться за стенами только для лицензированных разработчиков, как и большинство низкоуровневых деталей. Однако мы можем создать эквивалентную программу x86 и разобрать ее, чтобы получить общее представление.

Для начала посмотрим, что стоит беззнаковое расширение:

unsigned char x = 1;
unsigned int y = 1;
unsigned int z;
z = x;
z = y;

Соответствующая часть разбирается на (с помощью GCC 4.4.5):

    z = x;
  27:   0f b6 45 ff             movzbl -0x1(%ebp),%eax
  2b:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  2e:   8b 45 f8                mov    -0x8(%ebp),%eax
  31:   89 45 f4                mov    %eax,-0xc(%ebp)

Таким образом, в основном то же самое - в одном случае мы перемещаем байт, в другом мы перемещаем слово. Следующий:

signed char x = 1;
signed int y = 1;
signed int z;
z = x;
z = y;

Превращается в:

   z = x;
  11:   0f be 45 ff             movsbl -0x1(%ebp),%eax
  15:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  18:   8b 45 f8                mov    -0x8(%ebp),%eax
  1b:   89 45 f4                mov    %eax,-0xc(%ebp)

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

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

Это не переполнение стека, поэтому я надеюсь, что никто здесь не будет утверждать, что микрооптимизация не имеет значения. Игры часто работают с очень большими и очень числовыми данными, поэтому тщательное внимание к ветвлению, приведению, планированию, выравниванию структуры и т. Д. Может дать очень важные улучшения. Любой, кто потратил много времени на оптимизацию кода PPC, вероятно, имеет по крайней мере одну ужасную историю о загрузочных хит-магазинах. Но в этом случае это действительно не имеет значения. Размер хранилища целочисленного типа не влияет на производительность, если он выровнен и помещается в регистр.

user744
источник
2
(CW, потому что это на самом деле просто комментарий к ответу celion, и потому что мне любопытно, какие изменения в коде могут понадобиться людям, чтобы сделать его более наглядным.)
Информация о процессоре PS3 легко и легально доступна, поэтому обсуждение проблем с процессором, относящихся к PS3, не является проблемой. До тех пор, пока Sony не снимала поддержку OtherOS, любой мог привязать Linux к PS3 и запрограммировать его. GPU был недоступен, но с процессором (включая SPE) все в порядке. Даже без поддержки OtherOS вы можете легко получить соответствующий GCC и посмотреть, на что похож код.
JasonD
@Jason: я пометил свой пост как CW, поэтому, если кто-то делает это, он может предоставить информацию. Тем не менее, любой, кто имеет доступ к официальному компилятору Sony GameOS - который действительно является единственным, который имеет значение - вероятно, лишен этого права.
На самом деле подписанное целое число дороже на PPC IIRC. У него есть крошечный удар по производительности, но он есть ... также много деталей PS3 PPU / SPU здесь: jheriko-rtw.blogspot.co.uk/2011/07/ps3-ppuspu-docs.html и здесь: jheriko-rtw.blogspot.co.uk/2011/03/ppc-instruction-set.html . Любопытно, что это за компилятор GameOS? Это компилятор GCC или SNC? Кроме упомянутых выше вещей, у подписанных сравнений есть издержки, когда речь идет об оптимизации внутренних циклов. У меня нет доступа к документам, описывающим это, хотя - и даже если бы я сделал ...
jheriko
4

Целочисленные операции со знаком могут быть более дорогими практически на всех архитектурах. Например, деление на константу быстрее без знака, например:

unsigned foo(unsigned a) { return a / 1024U; }

будет оптимизирован для:

unsigned foo(unsigned a) { return a >> 10; }

Но...

int foo(int a) { return a / 1024; }

оптимизировать для:

int foo(int a) {
  return (a + 1023 * (a < 0)) >> 10;
}

или в системах, где разветвление дешево,

int foo(int a) {
  if (a >= 0) return a >> 10;
  else return (a + 1023) >> 10;
}

То же самое и по модулю. Это также верно для не-степеней-2 (но пример более сложный). Если ваша архитектура не имеет аппаратного деления (например, большинство ARM), беззнаковые деления неконстант также быстрее.

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

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

Джон Рипли
источник
Я отредактировал ваш оптимизированный код, чтобы больше отражать то, что на самом деле генерирует GCC, даже на -O0. Наличие ветки вводило в заблуждение, когда test + lea позволяет вам делать это без веток.
2
На х86, может быть. На ARMv7 он просто условно выполнен.
Джон Рипли
3

Операции со знаком или без знака int имеют одинаковую стоимость на текущих процессорах (x86_64, x86, powerpc, arm). На 32-битном процессоре u32, u16, u8 s32, s16, s8 должны быть одинаковыми. Вы можете иметь штраф с плохим выравниванием.

Но преобразование int в float или float в int является дорогостоящей операцией. Вы можете легко найти оптимизированную реализацию (SSE2, Neon ...).

Наиболее важным моментом, вероятно, является доступ к памяти. Если ваши данные не помещаются в кэш L1 / L2, вы потеряете больше цикла, чем конверсия.

Эллис
источник
2

Джон Пурди говорит выше (я не могу комментировать), что unsigned может быть медленнее, потому что он не может переполниться. Я не согласен, арифметика без знака - это простая арифметика Муляра по модулю 2 с количеством битов в слове. Подписанные операции в принципе могут быть переполнены, но обычно они отключены.

Иногда вы можете делать умные (но не очень удобочитаемые) вещи, такие как упаковка двух или более элементов данных в int и получение нескольких операций для каждой инструкции (карманная арифметика). Но ты должен понимать, что ты делаешь. Конечно, MMX позволяет делать это естественно. Но иногда использование самого большого размера слова, поддерживаемого HW, и ручная упаковка данных дают вам самую быструю реализацию.

Будьте осторожны с выравниванием данных. На большинстве реализаций HW не выровненные загрузки и хранилища работают медленнее. Естественное выравнивание означает, что, скажем, для 4-байтового слова адрес кратен четырем, а адреса из восьми байтов должны быть кратны восьми байтам. Это переносится в SSE (128-бит поддерживает 16-байтовое выравнивание). AVX скоро расширит эти «векторные» размеры регистров до 256 бит, а затем до 512 бит. И выровненные грузы / хранилища будут быстрее, чем выровненные. Для фанатов HW операция с невыровненной памятью может охватывать такие вещи, как кешлайн и даже границы страниц, для которых HW должен быть осторожен.


источник
1

Немного лучше использовать целые числа со знаком для индексов цикла, потому что переполнение со знаком не определено в C, поэтому компилятор будет предполагать, что такие циклы имеют меньше угловых случаев. Это контролируется gcc -fstrict-overflow (включено по умолчанию), и эффект, вероятно, трудно заметить, не читая вывод сборки.

Кроме того, x86 работает лучше, если вы не смешиваете типы, потому что он может использовать операнды памяти. Если он должен преобразовывать типы (знак или нулевые расширения), это означает явную загрузку и использование регистра.

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

Алекс странно
источник
0

Как указывает Целион, накладные расходы на преобразование между целыми числами и числами с плавающей запятой во многом связаны с копированием и преобразованием значений между регистрами. Единственные издержки неподписанных целочисленных объектов сами по себе связаны с их гарантированным поведением с циклическим изменением, что требует определенной проверки переполнения в скомпилированном коде.

В основном, нет необходимости в преобразовании целых чисел со знаком и без знака. Различные размеры целых чисел могут (бесконечно) быстрее или медленнее получать доступ в зависимости от платформы. Вообще говоря, размер целого числа, ближайший к размеру слова платформы, будет самым быстрым для доступа, но общая разница в производительности зависит от многих других факторов, в частности от размера кэша: если вы используете uint64_tвсе, что вам нужно uint32_t, это может будь то меньше ваших данных будет помещаться в кеш сразу, и вы можете понести некоторые накладные расходы.

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

Джон Перди
источник
К какой проверке переполнения вы обращаетесь? Если вы не имеете в виду уровень ниже, чем ассемблер, код для добавления двух целых чисел идентичен в большинстве систем, и на самом деле он не длиннее для тех немногих, которые используют, например, величину знака. Просто другой.
@JoeWreschnig: Черт. Кажется, я не могу его найти, но я знаю, что видел примеры различных выходных данных ассемблера, учитывающих определенное поведение обхода, по крайней мере на определенных платформах. Единственный связанный пост, который я смог найти: stackoverflow.com/questions/4712315/…
Джон Пурди,
Разные выходные данные ассемблера для различного поведения обхода заключаются в том, что компилятор может делать оптимизации в случае со знаком, например, если b> 0, тогда a + b> a, потому что переполнение со знаком не определено (и, таким образом, на него нельзя полагаться). Это действительно совершенно другая ситуация.