Производительность встроенных типов: char против short против in t против float против double
Это может показаться немного глупым вопросом, но, увидев ответ Александра С в другой теме, мне любопытно узнать, есть ли разница в производительности со встроенными типами:
char
противshort
противint
противfloat
противdouble
,
Обычно мы не учитываем такую разницу в производительности (если есть) в наших реальных проектах, но я хотел бы знать это для образовательных целей. Общие вопросы можно задать так:
Есть ли разница в производительности между интегральной арифметикой и арифметикой с плавающей точкой?
Что быстрее? В чем причина быть быстрее? Пожалуйста, объясните это.
9 ответов
Float против целого числа:
Исторически, с плавающей точкой может быть намного медленнее, чем целочисленная арифметика. На современных компьютерах это больше не так (на некоторых платформах это происходит несколько медленнее, но если вы не напишите идеальный код и не оптимизируете его для каждого цикла, разница будет подавлена другими недостатками в вашем коде).
На некоторых ограниченных процессорах, таких как в высокопроизводительных сотовых телефонах, с плавающей запятой может быть несколько медленнее, чем с целым числом, но обычно она находится в пределах порядка (или лучше), при условии, что доступна аппаратная с плавающей запятой. Стоит отметить, что этот разрыв довольно быстро сокращается, так как мобильные телефоны призваны выполнять все больше и больше общих вычислительных нагрузок.
На очень ограниченных процессорах (дешевые сотовые телефоны и тостер), как правило, нет аппаратного обеспечения с плавающей запятой, поэтому операции с плавающей запятой необходимо эмулировать в программном обеспечении. Это медленно - на пару порядков медленнее, чем целочисленная арифметика.
Как я уже сказал, люди ожидают, что их телефоны и другие устройства будут вести себя все больше и больше как "настоящие компьютеры", и разработчики оборудования быстро наращивают число процессоров, чтобы удовлетворить этот спрос. Если вы не гонитесь за каждым последним циклом или не пишете код для очень ограниченных процессоров, которые практически не поддерживают плавающую точку, различие в производительности для вас не имеет значения.
Различные целочисленные типы размеров:
Как правило, процессоры быстрее всего работают с целыми числами их собственного размера слова (с некоторыми оговорками о 64-битных системах). 32-разрядные операции часто выполняются быстрее, чем 8- или 16-разрядные операции на современных процессорах, но это довольно сильно различается в зависимости от архитектуры. Также помните, что вы не можете рассматривать скорость процессора изолированно; это часть сложной системы. Даже если работа с 16-разрядными числами в 2 раза медленнее, чем с 32-разрядными числами, вы можете поместить данные в иерархию кэша вдвое больше, если представите их с помощью 16-разрядных чисел вместо 32-разрядных. Если это делает разницу между тем, что все ваши данные поступают из кеша, а не с частыми пропусками кеша, то более быстрый доступ к памяти превзойдет медленную работу ЦП.
Другие заметки:
Векторизация способствует дальнейшему балансу в пользу более узких типов (float
и 8- и 16-битные целые числа) - вы можете выполнять больше операций с вектором одинаковой ширины. Тем не менее, хороший векторный код сложно написать, так что это не значит, что вы получите это преимущество без большой тщательной работы.
Почему существуют различия в производительности?
На самом деле есть только два фактора, которые влияют на то, является ли операция быстрой на процессоре: сложность схемы операции и потребность пользователя в быстрой операции.
(В пределах разумного) любая операция может быть сделана быстро, если разработчики микросхем готовы бросить достаточное количество транзисторов для решения проблемы. Но транзисторы стоят денег (или, скорее, использование большого количества транзисторов делает ваш чип больше, что означает, что вы получаете меньше чипов на пластину и меньший выход, что стоит денег), поэтому разработчики чипов должны сбалансировать, какую сложность использовать для каких операций, и они делают это на основе (предполагаемого) пользовательского спроса. Грубо говоря, вы можете разбить операции на четыре категории:
high demand low demand
high complexity FP add, multiply division
low complexity integer add popcount, hcf
boolean ops, shifts
Операции с высоким спросом и низкой сложностью будут быстрыми практически на любом процессоре: они являются низко висящим плодом и обеспечивают максимальную выгоду для пользователя на транзистор.
Операции с высоким спросом и высокой сложностью будут быстрыми на дорогих процессорах (например, используемых в компьютерах), потому что пользователи готовы платить за них. Вы, вероятно, не готовы платить дополнительные 3 доллара за тостер за быстрое умножение FP, поэтому дешевые процессоры будут экономить на этих инструкциях.
Операции с низким спросом и высокой сложностью, как правило, будут медленными почти на всех процессорах Там просто не достаточно выгоды, чтобы оправдать стоимость.
Операции с низким спросом и низкой сложностью будут быстрыми, если кто-то потрудится подумать о них, и несуществуют в противном случае.
Дальнейшее чтение:
- Agner Fog поддерживает хороший веб-сайт с множеством обсуждений низкоуровневых данных о производительности (и имеет очень научную методологию сбора данных, подтверждающую это).
- Справочное руководство по оптимизации архитектур Intel® 64 и IA-32 (ссылка на скачивание PDF находится на полпути вниз по странице) также охватывает многие из этих проблем, хотя и сфокусировано на одном конкретном семействе архитектур.
Абсолютно.
Во-первых, конечно, это полностью зависит от рассматриваемой архитектуры процессора.
Однако целочисленные типы и типы с плавающей запятой обрабатываются очень по-разному, поэтому почти всегда имеет место следующее:
- для простых операций целочисленные типы быстры. Например, сложение целых чисел часто имеет задержку только одного цикла, а умножение целых чисел обычно составляет около 2-4 циклов, IIRC.
- Типы с плавающей точкой раньше выполнялись намного медленнее. Однако на современных процессорах они имеют отличную пропускную способность, и каждый модуль с плавающей запятой обычно может отключать операции за цикл, что приводит к той же (или аналогичной) пропускной способности, что и для целочисленных операций. Тем не менее, задержка, как правило, хуже. Сложение с плавающей точкой часто имеет задержку около 4 циклов (против 1 для целых).
- для некоторых сложных операций ситуация другая или даже обратная. Например, деление на FP может иметь меньшую задержку, чем для целых чисел, просто потому, что операция сложна для реализации в обоих случаях, но она более полезна для значений FP, поэтому можно потратить больше усилий (и транзисторов) на оптимизацию этого случая.
На некоторых процессорах double может быть значительно медленнее, чем float. В некоторых архитектурах не предусмотрено выделенного оборудования для дубликатов, поэтому они обрабатываются путем пропускания двух блоков с плавающей запятой, что дает вам худшую пропускную способность и вдвое большую задержку. На других (например, x86 FPU) оба типа преобразуются в один и тот же внутренний формат (80-разрядная с плавающей запятой, в случае x86), поэтому производительность одинакова. В других же случаях как float, так и double имеют надлежащую аппаратную поддержку, но поскольку float имеет меньше битов, это можно сделать немного быстрее, обычно уменьшая задержку по сравнению с двойными операциями.
Отказ от ответственности: все упомянутые сроки и характеристики просто извлечены из памяти. Я ничего не нашел, так что это может быть неправильно.;)
Для разных целочисленных типов ответ сильно варьируется в зависимости от архитектуры процессора. Архитектура x86, благодаря своей длинной запутанной истории, должна поддерживать как 8, 16, 32 (и сегодня 64) битовые операции, так и в целом, все они одинаково быстры (они используют в основном то же оборудование, и только ноль). из верхних битов по мере необходимости).
Однако на других процессорах типы данных меньше, чем int
загрузка / хранение может быть более затратной (запись байта в память может потребоваться путем загрузки всего 32-битного слова, в котором оно находится, а затем выполнить битовую маскировку для обновления одного байта в регистре и затем записать целое слово назад). Аналогично, для типов данных больше int
некоторым процессорам, возможно, придется разделить операцию на две части, загружая / сохраняя / вычисляя нижнюю и верхнюю половины отдельно.
Но на x86 ответ таков: в основном это не имеет значения. По историческим причинам ЦП должен иметь достаточно надежную поддержку для каждого типа данных. Таким образом, единственное отличие, которое вы, вероятно, заметите, состоит в том, что операции с плавающей точкой имеют большую задержку (но схожую пропускную способность, поэтому они не медленнее сами по себе, по крайней мере, если вы пишете свой код правильно)
Я не думаю, что кто-то упомянул правила целочисленного продвижения. В стандартном C/C++ никакая операция не может быть выполнена для типа, меньшего чем int
, Если char или short на текущей платформе меньше, чем int, они неявно повышаются до int (что является основным источником ошибок). Для этого неявного продвижения требуется компилятор, без которого нельзя обойтись без стандарта.
Целочисленные продвижения означают, что никакая операция (сложение, побитовое, логическое и т. Д.) В языке не может происходить с меньшим целочисленным типом, чем int. Таким образом, операции над char /short / int, как правило, выполняются одинаково быстро, так как первые переходят в последние.
И помимо целочисленных повышений, есть "обычные арифметические преобразования", означающие, что C стремится сделать оба операнда одного типа, преобразовывая один из них в больший из двух, если они будут разными.
Однако ЦП может выполнять различные операции загрузки / сохранения на уровне 8, 16, 32 и т. Д. На 8- и 16-битных архитектурах это часто означает, что 8- и 16-битные типы быстрее, несмотря на целочисленные преобразования. На 32-битном процессоре это может означать, что меньшие типы работают медленнее, потому что он хочет, чтобы все было аккуратно выровнено в 32-битных блоках. 32-разрядные компиляторы обычно оптимизируют скорость и выделяют меньшие целочисленные типы в большем пространстве, чем указано.
Хотя обычно целочисленные типы меньшего размера занимают меньше места, чем большие, поэтому, если вы планируете оптимизировать объем оперативной памяти, они предпочитают.
Первый ответ выше велик, и я скопировал небольшой блок в следующий дубликат (так как именно там я и оказался первым).
"Char" и "small int" медленнее, чем "int"?
Я хотел бы предложить следующий код, который определяет распределение, инициализацию и выполнение некоторой арифметики для различных целочисленных размеров:
#include <iostream>
#include <windows.h>
using std::cout; using std::cin; using std::endl;
LARGE_INTEGER StartingTime, EndingTime, ElapsedMicroseconds;
LARGE_INTEGER Frequency;
void inline showElapsed(const char activity [])
{
QueryPerformanceCounter(&EndingTime);
ElapsedMicroseconds.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;
ElapsedMicroseconds.QuadPart *= 1000000;
ElapsedMicroseconds.QuadPart /= Frequency.QuadPart;
cout << activity << " took: " << ElapsedMicroseconds.QuadPart << "us" << endl;
}
int main()
{
cout << "Hallo!" << endl << endl;
QueryPerformanceFrequency(&Frequency);
const int32_t count = 1100100;
char activity[200];
//-----------------------------------------------------------------------------------------//
sprintf_s(activity, "Initialise & Set %d 8 bit integers", count);
QueryPerformanceCounter(&StartingTime);
int8_t *data8 = new int8_t[count];
for (int i = 0; i < count; i++)
{
data8[i] = i;
}
showElapsed(activity);
sprintf_s(activity, "Add 5 to %d 8 bit integers", count);
QueryPerformanceCounter(&StartingTime);
for (int i = 0; i < count; i++)
{
data8[i] = i + 5;
}
showElapsed(activity);
cout << endl;
//-----------------------------------------------------------------------------------------//
//-----------------------------------------------------------------------------------------//
sprintf_s(activity, "Initialise & Set %d 16 bit integers", count);
QueryPerformanceCounter(&StartingTime);
int16_t *data16 = new int16_t[count];
for (int i = 0; i < count; i++)
{
data16[i] = i;
}
showElapsed(activity);
sprintf_s(activity, "Add 5 to %d 16 bit integers", count);
QueryPerformanceCounter(&StartingTime);
for (int i = 0; i < count; i++)
{
data16[i] = i + 5;
}
showElapsed(activity);
cout << endl;
//-----------------------------------------------------------------------------------------//
//-----------------------------------------------------------------------------------------//
sprintf_s(activity, "Initialise & Set %d 32 bit integers", count);
QueryPerformanceCounter(&StartingTime);
int32_t *data32 = new int32_t[count];
for (int i = 0; i < count; i++)
{
data32[i] = i;
}
showElapsed(activity);
sprintf_s(activity, "Add 5 to %d 32 bit integers", count);
QueryPerformanceCounter(&StartingTime);
for (int i = 0; i < count; i++)
{
data32[i] = i + 5;
}
showElapsed(activity);
cout << endl;
//-----------------------------------------------------------------------------------------//
//-----------------------------------------------------------------------------------------//
sprintf_s(activity, "Initialise & Set %d 64 bit integers", count);
QueryPerformanceCounter(&StartingTime);
int64_t *data64 = new int64_t[count];
for (int i = 0; i < count; i++)
{
data64[i] = i;
}
showElapsed(activity);
sprintf_s(activity, "Add 5 to %d 64 bit integers", count);
QueryPerformanceCounter(&StartingTime);
for (int i = 0; i < count; i++)
{
data64[i] = i + 5;
}
showElapsed(activity);
cout << endl;
//-----------------------------------------------------------------------------------------//
getchar();
}
/*
My results on i7 4790k:
Initialise & Set 1100100 8 bit integers took: 444us
Add 5 to 1100100 8 bit integers took: 358us
Initialise & Set 1100100 16 bit integers took: 666us
Add 5 to 1100100 16 bit integers took: 359us
Initialise & Set 1100100 32 bit integers took: 870us
Add 5 to 1100100 32 bit integers took: 276us
Initialise & Set 1100100 64 bit integers took: 2201us
Add 5 to 1100100 64 bit integers took: 659us
*/
Мои результаты в MSVC на i7 4790k:
Инициализация и установка 1100100 8-битные целые числа заняли: 444us
Добавить от 5 до 1100100 8-битные целые числа заняли: 358us
Инициализация и установка 1100100 16-битные целые числа заняли: 666us
Добавить 5 к 1100100 16-битные целые числа заняли: 359us
Инициализация и установка 1100100 32-разрядные целые числа заняли: 870us
Добавить от 5 до 1100100 32-разрядные целые числа заняли: 276us
Инициализация и установка 1100100 64-разрядные целые числа заняли: 2201us
Добавить от 5 до 1100100 64-битные целые числа заняли: 659us
Есть ли разница в производительности между интегральной арифметикой и арифметикой с плавающей точкой?
Да. Тем не менее, это очень зависит от платформы и процессора. Разные платформы могут выполнять разные арифметические операции на разных скоростях.
При этом указанный ответ был более конкретным. pow()
это процедура общего назначения, которая работает с двойными значениями. Предоставляя ему целочисленные значения, он по-прежнему выполняет всю работу, которая потребуется для обработки нецелых показателей. Использование прямого умножения позволяет обойти большую сложность, и именно здесь скорость вступает в игру. Это на самом деле не проблема (так много) разных типов, а скорее обход большого количества сложного кода, необходимого для работы функции pow с любым показателем степени.
Как правило, целочисленная математика быстрее, чем математика с плавающей точкой. Это потому, что целочисленная математика включает в себя более простые вычисления. Однако в большинстве операций речь идет о менее чем дюжине часов. Не миллис, микро, нанос или клещей; часы. Те, которые происходят между 2-3 миллиардами раз в секунду в современных ядрах. Кроме того, поскольку у 486 много ядер имеют набор модулей обработки с плавающей запятой или FPU, которые жестко привязаны для эффективного выполнения арифметики с плавающей запятой и часто параллельно с процессором.
В результате этого, хотя технически это медленнее, вычисления с плавающей точкой все еще настолько быстры, что любая попытка рассчитать разницу будет иметь больше ошибок, связанных с механизмом синхронизации и планированием потоков, чем на самом деле требуется для выполнения вычислений. Используйте целочисленные значения, когда можете, но понимайте, когда не можете, и не слишком беспокоитесь об относительной скорости вычислений.
Зависит от состава процессора и платформы.
Платформы с сопроцессором с плавающей запятой могут быть медленнее, чем интегральная арифметика, из-за того, что значения должны передаваться в сопроцессор и из него.
Если обработка с плавающей запятой находится в ядре процессора, время выполнения может быть незначительным.
Если вычисления с плавающей точкой эмулируются программным обеспечением, то интегральная арифметика будет быстрее.
Если есть сомнения, профиль.
Заставьте программирование работать правильно и надежно перед оптимизацией.
Конечно, есть разница между арифметикой с плавающей точкой и целочисленной. В зависимости от конкретного аппаратного обеспечения процессора и микрокоманд вы получаете разную производительность и / или точность. Хорошие термины Google для точных описаний (я тоже точно не знаю):
FPU x87 MMX SSE
Что касается размера целых чисел, лучше всего использовать размер слова платформы / архитектуры (или удвоить его), что сводится к int32_t
на х86 и int64_t
на x86_64. У процессоров SOme могут быть встроенные инструкции, которые обрабатывают несколько из этих значений одновременно (например, SSE (с плавающей запятой) и MMX), что ускорит параллельное сложение или умножение.
Нет, не совсем. Это, конечно, зависит от процессора и компилятора, но разница в производительности, как правило, незначительна, если она вообще есть.