Производительность встроенных типов: char против short против int против float против double - PullRequest
64 голосов
/ 21 февраля 2011

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

char против short против int против float против double.

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

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

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

Ответы [ 9 ]

112 голосов
/ 21 февраля 2011

Число с плавающей запятой против целого числа:

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

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

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

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

Целочисленные типы различных размеров:

Как правило, ЦП быстрее всего работают с целыми числами их собственного размера слова (с некоторыми оговорками о 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 находится на полпути вниз по странице) также охватывает многие из этих проблем, хотя сфокусировано на одной конкретной группе архитектур.
11 голосов
/ 21 февраля 2011

Абсолютно.

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

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

  • для простых операций, интегральные типы: fast . Например, сложение целых чисел часто имеет задержку только одного цикла, а умножение целых чисел обычно составляет около 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 ответ таков: это в основном не имеет значения. По историческим причинам ЦП должен иметь достаточно надежную поддержку для каждого типа данных. Таким образом, единственное отличие, которое вы, вероятно, заметите, состоит в том, что операции с плавающей точкой имеют большую задержку (но схожую пропускную способность, поэтому они не являются на медленнее сами по себе, по крайней мере, если вы пишете свой код правильно) *

7 голосов
/ 22 февраля 2011

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

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

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

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

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

2 голосов
/ 03 мая 2016

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

"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

2 голосов
/ 21 февраля 2011

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

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

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

1 голос
/ 21 февраля 2011

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

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

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

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

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

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

0 голосов
/ 21 февраля 2011

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

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

0 голосов
/ 21 февраля 2011

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

FPU x87 MMX SSE

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

0 голосов
/ 21 февраля 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...