Производительность встроенных типов: голец против короткие против Инт и флоат и дабл


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

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

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

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

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

9 59

9 ответов:

число с плавающей точкой и целое число:

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

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

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

другой размер целое число типы:

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

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

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

читайте далее:

  • Агнер туман поддерживает хороший сайт с большим количеством обсуждения деталей низкого уровня производительности (и имеет очень научную методологию сбора данных для поддержки это вверх).
  • справочное руководство по оптимизации архитектур Intel® 64 и IA-32 (ссылка для загрузки PDF-это часть страницы) также охватывает многие из этих проблем, хотя она сосредоточена на одном конкретном семействе архитектур.

абсолютно.

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

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

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

на некоторых процессорах удвоение может быть значительно медленнее, чем плавает. На некоторых архитектурах нет специального оборудования для двойников, и поэтому они обрабатываются путем передачи двух кусков размером с поплавок, что дает вам худшее пропускная способность и вдвое большая задержка. На других (например, 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 уровня etc. На 8 - и 16-битных архитектур, это часто означает, что 8 и 16 битные типы быстрее, несмотря на целое число акций. На 32-битном процессоре это может фактически означать, что меньшие типы медленнее, потому что он хочет, чтобы все аккуратно выстроились в 32-битных блоков. 32-разрядные компиляторы обычно оптимизируют скорость и выделяют меньшие целочисленные типы в большем пространстве, чем указано.

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

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

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

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

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

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

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

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

Если вы сомневаетесь, профиль.

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

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

являются ли "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

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

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

FPU x87 MMX SSE

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

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

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