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

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

В нашей библиотеке С++ в настоящее время используется time_t для хранения значений времени. В некоторых местах мне нужна точная секундная точность, поэтому в любом случае вам понадобится больший тип данных. Кроме того, было бы полезно обойти проблему Год-2038 в некоторых местах. Поэтому я думаю о полном переключении на один временной класс с базовым значением 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).

4b9b3361

Ответ 1

который влияет на эффективность этих операций? Вероятно, компилятор и версия компилятора; но работает ли операционная система или CPU make/model влияет на это также?

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

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

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

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

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

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

Mulitplication будет намного хуже, если входные параметры на самом деле являются 64-битными - так что 2 ^ 35 * 83 хуже, чем 2 ^ 31 * 2 ^ 31, например. Это связано с тем, что процессор может производить 32 x 32-битное умножение на 64-битный результат довольно хорошо - около 5-10 тактов. Но для 64-битного 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

И результаты: Примечание. См. ниже результаты. Эти результаты немного оптимистичны из-за различий в использовании инструкций 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

Ответ 2

Больше, чем вы хотели знать о выполнении 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 из результата, если бит переноса не установлен.

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

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

  • Добавить сначала 32 бита b в первые 32 бита a.
  • Добавить с переносом позже 32 бит b в более поздние 32 бита.

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

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

Умножение

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

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

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

a 1 ≡ b 1 (mod n)
a 2 ≡ b 2 (mod n)
a 1 a 2 ≡ b 1 b 2 (mod n)

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

  • Более 32 бит умножения как младших 32 бит (переполнение, которое может хранить CPU в edx)
  • Более 32 бит первой переменной mulitplied с более низкими 32 битами второй переменной
  • Нижние 32 бита первой переменной, умноженной на более высокие 32 бита второй переменной

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

Division/Modulo (оба варианта реализованы)

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

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

Или/и/Xor

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

Смещение влево/вправо

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

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

__ builtin_popcount [LL]

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

Другие встроенные функции

Я слишком ленив, чтобы закончить ответ в этот момент. Кто-нибудь даже использует их?

Unsigned vs signed

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

Бенчмарки

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

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

Ответ 3

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

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

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

Ответ 4

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

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