Подтвердить что ты не робот

Производительность встроенных типов: char vs short vs int vs. float vs. double

Это может показаться немного глупым вопросом, но, увидев Alexandre C ответ в другой теме, мне любопытно узнать, что если есть какая-либо разница в производительности с встроенные типы:

char vs short vs int vs. floatvs. double.

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

  • Есть ли разница в производительности между интегральной арифметикой и арифметикой с плавающей запятой?

  • Что быстрее? В чем причина быстрого? Пожалуйста, объясните это.

4b9b3361

Ответ 1

Float vs. integer:

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

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

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

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

Различные типы целого числа:

Как правило, процессоры быстрее всего работают на целых числах их собственного размера слова (с некоторыми оговорками о 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

Совершенно верно.

Во-первых, конечно, это полностью зависит от архитектуры процессора.

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

  • для простых операций, интегральные типы бывают быстрыми. Например, целочисленное добавление часто имеет только задержку одного цикла, а целочисленное умножение обычно составляет около 2-4 циклов, IIRC.
  • Типы с плавающей точкой, используемые для выполнения намного медленнее. Однако на сегодняшних процессорах они обладают превосходной пропускной способностью, и каждый блок с плавающей запятой обычно может удалять операцию за цикл, что приводит к той же (или аналогичной) пропускной способности, что и для целых операций. Однако латентность, как правило, хуже. Добавление с плавающей запятой часто имеет задержку около 4 циклов (vs 1 для ints).
  • для некоторых сложных операций, ситуация другая или даже отменена. Например, деление на FP может иметь меньшую задержку, чем для целых чисел, просто потому, что операция сложна для реализации в обоих случаях, но она более полезна для значений FP, поэтому можно потратить больше усилий (и транзисторов) на оптимизацию этого случая.

На некоторых процессорах удвоение может быть значительно медленнее, чем плавающие. На некоторых архитектурах нет специального оборудования для парных разрядов, и поэтому они обрабатываются путем пропускания двух блоков размера поплавка, что дает вам более высокую пропускную способность и вдвое большую задержку. В других случаях (например, xPU x86) оба типа преобразуются в один и тот же внутренний формат с 80-битной плавающей запятой, в случае x86), поэтому производительность идентична. В других случаях, как float, так и double имеют надлежащую аппаратную поддержку, но поскольку float имеет меньшее количество бит, это может быть сделано немного быстрее, обычно уменьшая задержку бит относительно двойных операций.

Отказ от ответственности: все упомянутые тайминги и характеристики просто извлекаются из памяти. Я не смотрел ни на что, так что это может быть неправильно.;)

Для разных целых типов ответ сильно зависит от архитектуры процессора. Архитектура x86, из-за ее длинной запутанной истории, должна поддерживать как 8, 16, 32 (и сегодня 64) битовые операции изначально, так и в целом, они все одинаково быстрые (они используют в основном одно и то же оборудование и только нулевые если необходимо).

Однако на других процессорах типы данных, меньшие, чем int, могут быть более дорогостоящими для загрузки/хранения (запись байта в память может быть выполнена путем загрузки всего 32-битного слова, в котором оно находится, а затем make bit masking, чтобы обновить один байт в регистре, а затем записать все слова назад). Аналогично, для типов данных, превышающих int, некоторым процессорам, возможно, придется разделить операцию на два, погрузка/хранение/вычисление нижней и верхней половин отдельно.

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

Ответ 3

Я не думаю, что кто-то упомянул целые правила продвижения. В стандартном C/С++ никакая операция не может выполняться с типом меньше int. Если char или short меньше, чем int на текущей платформе, они неявно повышаются до int (что является основным источником ошибок). Компилятор должен сделать это неявное продвижение, там нет пути вокруг него, не нарушая стандарт.

Целочисленные акции означают, что никакая операция (добавление, побитовое, логическое и т.д. и т.д.) в языке не может выполняться на меньшем целочисленном типе, чем int. Таким образом, операции с char/short/int, как правило, одинаково быстры, так как первые повышаются до последнего.

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

Однако CPU может выполнять различные операции загрузки/хранения на уровне 8, 16, 32 и т.д. В 8- и 16-разрядных архитектурах это часто означает, что 8 и 16-разрядные типы быстрее, несмотря на целые рекламные акции. На 32-битном процессоре это может означать, что более мелкие типы медленнее, потому что он хочет, чтобы все было аккуратно выровнено в 32-битных кусках. 32-битные компиляторы обычно оптимизируют скорость и выделяют меньшие целые типы в большем пространстве, чем указано.

Хотя обычно меньшие целые типы, конечно, занимают меньше места, чем более крупные, поэтому, если вы планируете оптимизировать размер ОЗУ, они предпочитают.

Ответ 4

Есть ли разница в производительности между интегральной арифметикой и арифметикой с плавающей запятой?

Да. Однако это очень специфическая платформа и процессор. Различные платформы могут выполнять различные арифметические операции на разных скоростях.

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

Ответ 5

Зависит от состава процессора и платформы.

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

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

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

Если есть сомнения, профиль.

Обеспечьте правильное и надежное программирование перед оптимизацией.

Ответ 6

Нет, не совсем. Это, конечно, зависит от процессора и компилятора, но разница в производительности обычно незначительна, даже если она есть.

Ответ 7

Существует определенная разница между арифметикой с плавающей точкой и целым числом. В зависимости от аппаратного обеспечения и микро-инструкций для процессора вы получаете разную производительность и/или точность. Хорошие термины google для точных описаний (я точно не знаю):

FPU x87 MMX SSE

Что касается размера целых чисел, лучше всего использовать размер слова платформы/архитектуры (или двойной), который сводится к int32_t на x86 и int64_t на x86_64. Процессоры SOme могут иметь встроенные инструкции, которые обрабатывают сразу несколько таких значений (например, SSE (с плавающей запятой) и MMX), что ускорит параллельные добавления или умножения.

Ответ 8

Как правило, целочисленная математика быстрее, чем математика с плавающей запятой. Это связано с тем, что целочисленная математика включает в себя более простые вычисления. Однако в большинстве операций мы говорим о менее чем десятке часов. Не миллины, микроны, нано или тики; часы. Те, которые происходят в 2-3 миллиарда раз в секунду в современных ядрах. Кроме того, поскольку в 486 множество ядер имеют набор модулей обработки с плавающей запятой или FPU, которые жестко подключены для эффективной арифметики с плавающей запятой и часто параллельно с процессором.

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

Ответ 9

Первый ответ выше, и я скопировал небольшой блок из него в следующий дубликат (так как именно здесь я оказался первым).

Есть "char" и "малый 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 to 1100100 8-битных целых чисел: 358us

Инициализировать и установить 1100100 16-битных целых чисел: 666us
Добавить 5 до 1100100 16-битных целых чисел: 359us

Инициализировать и установить 1100100 32-битных целых чисел: 870us
Добавить 5 to 1100100 32-битных целых чисел: 276us

Инициализировать и установить 1100100 64-битных целых чисел: 2201us
Добавить 5 to 1100100 64-битных целых чисел: 659us