Как влияет на производительность использование int64_t вместо int32_t в 32-битных системах?

Наша библиотека C ++ в настоящее время использует time_t для хранения значений времени. В некоторых местах я начинаю нуждаться в точности до секунды, поэтому в любом случае потребуется больший тип данных. Кроме того, может быть полезно обойти проблему Года-2038 в некоторых местах. Поэтому я думаю о полном переключении на один класс Time с базовым значением int64_t, чтобы заменить значение time_t во всех местах.

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

Что меня интересует:

  • Какие факторы влияют на выполнение этих операций? Вероятно, версия компилятора и компилятора; но влияет ли на это операционная система или производитель / модель процессора? Будет ли обычная 32-разрядная система использовать 64-разрядные регистры современных процессоров?
  • какие операции будут особенно медленными при эмуляции на 32-битной версии? Или что почти не замедлится?
  • Существуют ли какие-либо результаты тестов для использования int64_t / uint64_t в 32-битных системах?
  • У кого-нибудь есть собственный опыт об этом влиянии на производительность?

В основном меня интересуют g ++ 4.1 и 4.4 в Linux 2.6 (RHEL5, RHEL6) в системах Intel Core 2; но было бы также неплохо узнать о ситуации для других систем (таких как Sparc Solaris + Solaris CC, Windows + MSVC).

49

Решение

Какие факторы влияют на выполнение этих операций? Вероятно,
версия компилятора и компилятора; но операционная система или
Изготовитель / модель процессора также влияют на это?

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

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

Будет ли обычная 32-разрядная система использовать 64-разрядные регистры современных процессоров?

Это невозможно. Если система находится в 32-битном режиме, она будет действовать как 32-битная система, дополнительные 32-битные регистры полностью невидимы, как если бы система была «истинной 32-битной системой». ,

какие операции будут особенно медленными при эмуляции на 32-битной версии? Или что почти не замедлится?

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

Мультипликация станет намного хуже, если входные параметры на самом деле будут 64-битными, поэтому, например, 2 ^ 35 * 83 хуже, чем 2 ^ 31 * 2 ^ 31. Это связано с тем фактом, что процессор может довольно хорошо производить умножение 32 x 32 бита на 64-битный результат — около 5-10 тактов. Но умножение на 64 x 64 бита требует значительного дополнительного кода, поэтому займет больше времени.

Деление — это та же проблема, что и для умножения, но здесь можно взять 64-разрядный вход с одной стороны, разделить его на 32-разрядное значение и получить 32-разрядное значение. Поскольку трудно предсказать, когда это сработает, 64-разрядное разделение, вероятно, почти всегда медленное.

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

Компилятору также нужно будет использовать больше регистров.

Существуют ли какие-либо результаты тестов для использования int64_t / uint64_t в 32-битных системах?

Возможно, но я не знаю ни о чем. И даже если они есть, это будет для вас только несколько значимым, поскольку сочетание операций ОЧЕНЬ критично для скорости операций.

Если производительность является важной частью вашего приложения, тогда сравните ВАШ код (или некоторую его часть). На самом деле не имеет значения, дает ли Benchmark X результаты на 5%, 25% или 103% медленнее, если ваш код несколько отличается по объему медленнее или быстрее при тех же обстоятельствах.

У кого-нибудь есть собственный опыт об этом влиянии на производительность?

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

Может быть, поможет переход с вашей ОС на 64-битную версию той же самой ОС?

Редактировать:

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

Вот код, который я написал, пытаясь воспроизвести несколько общих функций:

#include <iostream>
#include <cstdint>
#include <ctime>

using namespace std;

static __inline__ uint64_t rdtsc(void)
{
unsigned hi, lo;
__asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
return ( (uint64_t)lo)|( ((uint64_t)hi)<<32 );
}

template<typename T>
static T add_numbers(const T *v, const int size)
{
T sum = 0;
for(int i = 0; i < size; i++)
sum += v[i];
return sum;
}template<typename T, const int size>
static T add_matrix(const T v[size][size])
{
T sum[size] = {};
for(int i = 0; i < size; i++)
{
for(int j = 0; j < size; j++)
sum[i] += v[i][j];
}
T tsum=0;
for(int i = 0; i < size; i++)
tsum += sum[i];
return tsum;
}template<typename T>
static T add_mul_numbers(const T *v, const T mul, const int size)
{
T sum = 0;
for(int i = 0; i < size; i++)
sum += v[i] * mul;
return sum;
}

template<typename T>
static T add_div_numbers(const T *v, const T mul, const int size)
{
T sum = 0;
for(int i = 0; i < size; i++)
sum += v[i] / mul;
return sum;
}template<typename T>
void fill_array(T *v, const int size)
{
for(int i = 0; i < size; i++)
v[i] = i;
}

template<typename T, const int size>
void fill_array(T v[size][size])
{
for(int i = 0; i < size; i++)
for(int j = 0; j < size; j++)
v[i][j] = i + size * j;
}uint32_t bench_add_numbers(const uint32_t v[], const int size)
{
uint32_t res = add_numbers(v, size);
return res;
}

uint64_t bench_add_numbers(const uint64_t v[], const int size)
{
uint64_t res = add_numbers(v, size);
return res;
}

uint32_t bench_add_mul_numbers(const uint32_t v[], const int size)
{
const uint32_t c = 7;
uint32_t res = add_mul_numbers(v, c, size);
return res;
}

uint64_t bench_add_mul_numbers(const uint64_t v[], const int size)
{
const uint64_t c = 7;
uint64_t res = add_mul_numbers(v, c, size);
return res;
}

uint32_t bench_add_div_numbers(const uint32_t v[], const int size)
{
const uint32_t c = 7;
uint32_t res = add_div_numbers(v, c, size);
return res;
}

uint64_t bench_add_div_numbers(const uint64_t v[], const int size)
{
const uint64_t c = 7;
uint64_t res = add_div_numbers(v, c, size);
return res;
}template<const int size>
uint32_t bench_matrix(const uint32_t v[size][size])
{
uint32_t res = add_matrix(v);
return res;
}
template<const int size>
uint64_t bench_matrix(const uint64_t v[size][size])
{
uint64_t res = add_matrix(v);
return res;
}template<typename T>
void runbench(T (*func)(const T *v, const int size), const char *name, T *v, const int size)
{
fill_array(v, size);

uint64_t long t = rdtsc();
T res = func(v, size);
t = rdtsc() - t;
cout << "result = " << res << endl;
cout << name << " time in clocks " << dec << t  << endl;
}

template<typename T, const int size>
void runbench2(T (*func)(const T v[size][size]), const char *name, T v[size][size])
{
fill_array(v);

uint64_t long t = rdtsc();
T res = func(v);
t = rdtsc() - t;
cout << "result = " << res << endl;
cout << name << " time in clocks " << dec << t  << endl;
}int main()
{
// spin up CPU to full speed...
time_t t = time(NULL);
while(t == time(NULL)) ;

const int vsize=10000;

uint32_t v32[vsize];
uint64_t v64[vsize];

uint32_t m32[100][100];
uint64_t m64[100][100];runbench(bench_add_numbers, "Add 32", v32, vsize);
runbench(bench_add_numbers, "Add 64", v64, vsize);

runbench(bench_add_mul_numbers, "Add Mul 32", v32, vsize);
runbench(bench_add_mul_numbers, "Add Mul 64", v64, vsize);

runbench(bench_add_div_numbers, "Add Div 32", v32, vsize);
runbench(bench_add_div_numbers, "Add Div 64", v64, vsize);

runbench2(bench_matrix, "Matrix 32", m32);
runbench2(bench_matrix, "Matrix 64", m64);
}

Составлено с:

g++ -Wall -m32 -O3 -o 32vs64 32vs64.cpp -std=c++0x

И результаты: Примечание: результаты 2016 года ниже — Эти результаты немного оптимистичны из-за разницы в использовании инструкций SSE в 64-битном режиме, но не в использовании SSE в 32-битном режиме.

result = 49995000
Add 32 time in clocks 20784
result = 49995000
Add 64 time in clocks 30358
result = 349965000
Add Mul 32 time in clocks 30182
result = 349965000
Add Mul 64 time in clocks 79081
result = 7137858
Add Div 32 time in clocks 60167
result = 7137858
Add Div 64 time in clocks 457116
result = 49995000
Matrix 32 time in clocks 22831
result = 49995000
Matrix 64 time in clocks 23823

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

И это быстрее на 64-битной, я слышал, некоторые из вас спрашивают:
Используя те же параметры компилятора, просто -m64 вместо -m32 — yupp, намного быстрее:

result = 49995000
Add 32 time in clocks 8366
result = 49995000
Add 64 time in clocks 16188
result = 349965000
Add Mul 32 time in clocks 15943
result = 349965000
Add Mul 64 time in clocks 35828
result = 7137858
Add Div 32 time in clocks 50176
result = 7137858
Add Div 64 time in clocks 50472
result = 49995000
Matrix 32 time in clocks 12294
result = 49995000
Matrix 64 time in clocks 14733

Редактирование, обновление на 2016 год:
четыре варианта, с SSE и без, в 32- и 64-битном режиме компилятора.

Я обычно использую clang ++ в качестве моего обычного компилятора в эти дни. Я попытался скомпилировать с помощью g ++ (но это все равно будет версия, отличная от описанной выше, поскольку я обновил свой компьютер — и у меня тоже другой процессор). Так как g ++ не смог скомпилировать версию no-sse в 64-битной версии, я не видел в этом смысла. (g ++ в любом случае дает похожие результаты)

В качестве короткого стола:

Test name      | no-sse 32 | no-sse 64 | sse 32 | sse 64 |
----------------------------------------------------------
Add uint32_t   |   20837   |   10221   |   3701 |   3017 |
----------------------------------------------------------
Add uint64_t   |   18633   |   11270   |   9328 |   9180 |
----------------------------------------------------------
Add Mul 32     |   26785   |   18342   |  11510 |  11562 |
----------------------------------------------------------
Add Mul 64     |   44701   |   17693   |  29213 |  16159 |
----------------------------------------------------------
Add Div 32     |   44570   |   47695   |  17713 |  17523 |
----------------------------------------------------------
Add Div 64     |  405258   |   52875   | 405150 |  47043 |
----------------------------------------------------------
Matrix 32      |   41470   |   15811   |  21542 |   8622 |
----------------------------------------------------------
Matrix 64      |   22184   |   15168   |  13757 |  12448 |

Полные результаты с опциями компиляции.

$ clang++ -m32 -mno-sse 32vs64.cpp --std=c++11 -O2
$ ./a.out
result = 49995000
Add 32 time in clocks 20837
result = 49995000
Add 64 time in clocks 18633
result = 349965000
Add Mul 32 time in clocks 26785
result = 349965000
Add Mul 64 time in clocks 44701
result = 7137858
Add Div 32 time in clocks 44570
result = 7137858
Add Div 64 time in clocks 405258
result = 49995000
Matrix 32 time in clocks 41470
result = 49995000
Matrix 64 time in clocks 22184

$ clang++ -m32 -msse 32vs64.cpp --std=c++11 -O2
$ ./a.out
result = 49995000
Add 32 time in clocks 3701
result = 49995000
Add 64 time in clocks 9328
result = 349965000
Add Mul 32 time in clocks 11510
result = 349965000
Add Mul 64 time in clocks 29213
result = 7137858
Add Div 32 time in clocks 17713
result = 7137858
Add Div 64 time in clocks 405150
result = 49995000
Matrix 32 time in clocks 21542
result = 49995000
Matrix 64 time in clocks 13757$ clang++ -m64 -msse 32vs64.cpp --std=c++11 -O2
$ ./a.out
result = 49995000
Add 32 time in clocks 3017
result = 49995000
Add 64 time in clocks 9180
result = 349965000
Add Mul 32 time in clocks 11562
result = 349965000
Add Mul 64 time in clocks 16159
result = 7137858
Add Div 32 time in clocks 17523
result = 7137858
Add Div 64 time in clocks 47043
result = 49995000
Matrix 32 time in clocks 8622
result = 49995000
Matrix 64 time in clocks 12448$ clang++ -m64 -mno-sse 32vs64.cpp --std=c++11 -O2
$ ./a.out
result = 49995000
Add 32 time in clocks 10221
result = 49995000
Add 64 time in clocks 11270
result = 349965000
Add Mul 32 time in clocks 18342
result = 349965000
Add Mul 64 time in clocks 17693
result = 7137858
Add Div 32 time in clocks 47695
result = 7137858
Add Div 64 time in clocks 52875
result = 49995000
Matrix 32 time in clocks 15811
result = 49995000
Matrix 64 time in clocks 15168
46

Другие решения

Больше, чем вы когда-либо хотели знать о выполнении 64-битной математики в 32-битном режиме …

Когда вы используете 64-разрядные числа в 32-разрядном режиме (даже на 64-разрядном ЦП, если код скомпилирован для 32-разрядных), они сохраняются как два отдельных 32-разрядных числа, одно из которых хранит старшие биты числа, и другой хранит младшие биты. Влияние этого зависит от инструкции. (tl; dr — как правило, выполнение 64-битной математики на 32-битном ЦП теоретически в 2 раза медленнее, если вы не делите / по модулю, однако на практике разница будет меньше (1.3x будет моим думаю), потому что обычно программы не просто выполняют математику с 64-битными целыми числами, а также из-за конвейерной разницы, разница может быть намного меньше в вашей программе).

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

  C 7 6 5 4 3 2 1 0      C 7 6 5 4 3 2 1 0
0 1 1 1 1 1 1 1 1      1 0 0 0 0 0 0 0 0
+   0 0 0 0 0 0 0 1    -   0 0 0 0 0 0 0 1
= 1 0 0 0 0 0 0 0 0    = 0 1 1 1 1 1 1 1 1

Почему флаг переноса имеет значение? Что ж, так получилось, что процессоры обычно выполняют две отдельные операции сложения и вычитания. В x86 операции сложения называются add а также adc, add обозначает дополнение, в то время как adc для добавления с переносом. Разница между ними в том, что adc считает бит переноса, и если он установлен, он добавляет один к результату.

Аналогично, вычитание с переносом вычитает 1 из результата, если бит переноса не установлен.

Такое поведение позволяет легко реализовать сложение и вычитание произвольного размера на целых числах. Результат сложения Икс а также Y (при условии, что они 8-битные) никогда не бывает больше, чем 0x1FE, Если вы добавите 1, ты получаешь 0x1FF, Поэтому 9 бит достаточно для представления результатов любого 8-битного сложения. Если вы начнете дополнение с add, а затем добавить любые биты, кроме начальных, с помощью adcВы можете сделать дополнение на любой размер данных, которые вам нравятся.

Добавление двух 64-битных значений на 32-битном процессоре происходит следующим образом.

  1. Добавьте первые 32 бита б до первых 32 бит .
  2. добавлять с переносом спустя 32 бита б позже 32 бита .

Аналогично для вычитания.

Это дает 2 инструкции, однако, из-за инструкция конвейерной обработки, это может быть медленнее, чем это, так как один расчет зависит от другого, чтобы закончить, поэтому, если CPU не имеет ничего другого, кроме 64-битного добавления, CPU может ждать, пока будет выполнено первое добавление.

На x86 так получилось, что imul а также mul может быть использован таким образом, что переполнение сохраняется в EDX регистр. Поэтому умножение двух 32-битных значений для получения 64-битного значения действительно легко. Такое умножение является одной инструкцией, но чтобы использовать ее, одно из значений умножения должно быть сохранено в е.

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

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

1б1 (моды N)
2б2 (моды N)
12б1б2 (моды N)

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

  • Старшие 32 бита умножения обоих младших 32 битов (переполнение, которое может хранить CPU EDX)
  • Старшие 32 бита первой переменной, умноженные на младшие 32 бита второй переменной
  • Младшие 32 бита первой переменной, умноженные на старшие 32 бита второй переменной

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

Я не знаю, как это работает, но это намного сложнее, чем сложение, вычитание или даже умножение. Однако, вероятно, это будет в десять раз медленнее, чем деление на 64-битном процессоре. Обратитесь к разделу «Искусство компьютерного программирования», том 2: Получисленные алгоритмы », стр. 257, чтобы узнать подробности, если вы можете это понять (я, к сожалению, не могу этого объяснить).

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

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

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

Однако это только случай постоянных сдвигов. Когда сдвиг не является постоянным, все становится сложнее, поскольку архитектура x86 поддерживает только сдвиг со значением 0-31. Все, что выходит за рамки этого, в соответствии с официальной документацией не определено, и на практике побитовое и операция с 0x1F выполняется над значением. Поэтому, когда значение сдвига больше 31, одно из хранилищ значений полностью удаляется (для левого сдвига это младшие байты, для правого сдвига это старшие байты). Другой получает значение, которое было в регистре, который был удален, и затем выполняется операция сдвига. В результате это зависит от предсказателя ветвления, который делает хорошие прогнозы, и немного медленнее, потому что необходимо проверить значение.

__builtin_popcount (ниже) + __builtin_popcount (выше)

Мне лень закончить ответ на этом этапе. Кто-нибудь вообще этим пользуется?

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

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

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

8

Ваш вопрос звучит довольно странно в его окружении. Вы используете time_t, который использует до 32 бит. Вам нужна дополнительная информация, что означает больше битов. Так что вы вынуждены использовать что-то большее, чем int32. Неважно, какова производительность, верно? Выбор будет между использованием всего лишь 40 бит или переходом к int64. Если не хранить миллионы экземпляров, последний является разумным выбором.

Как отмечали другие, единственный способ узнать истинную производительность — это измерить ее с помощью профилировщика (в некоторых грубых примерах подойдут простые часы). так что просто идти вперед и измерить. Должно быть нетрудно глобально заменить ваше использование time_t на typedef, переопределить его на 64-битное и исправить несколько случаев, когда ожидалось реальное время.

Моя ставка будет на «неизмеримую разницу», если ваши текущие экземпляры time_t не займут хотя бы несколько мегабайт памяти. на современных Intel-подобных платформах ядра проводят большую часть времени в ожидании попадания внешней памяти в кеш. Один тайник пропускает сто (и) циклов. Что делает невозможным вычисление разниц в 1 такт по инструкциям. Ваша реальная производительность может снизиться из-за того, что ваша текущая структура просто соответствует размеру строки кэша, а большей — две. И если вы никогда не измеряли свою текущую производительность, вы могли бы обнаружить, что вы можете получить экстремальное ускорение некоторых функций, просто добавив некоторое выравнивание или порядок обмена некоторых членов в структуре. Или упакуйте (1) структуру вместо использования макета по умолчанию …

2

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

Обратите внимание, что Intel Core 2 поддерживает EM64T.

0
По вопросам рекламы [email protected]