У меня есть массив байтов (unsigned char *
), которое должно быть преобразовано в целое число. Целое число представлено более трех байтов. Это то, что я сделал
//bytes array is allocated and filled
//allocating space for intBuffer (uint32_t)
unsigned long i = 0;
uint32_t number;
for(; i<size_tot; i+=3){
uint32_t number = (bytes[i]<<16) | (bytes[i+1]<<8) | bytes[i+2];
intBuffer[number]++;
}
Этот кусок кода хорошо выполняет свою работу, но он невероятно медленный из-за трех обращений к памяти (особенно для больших значений size_tot
, в порядке 3000000
). Есть ли способ сделать это быстрее и повысить производительность?
Правильный ответ почти всегда:
Напишите правильный код, включите оптимизацию, доверьтесь компилятору.
дано:
void count_values(std::array<uint32_t, 256^3>& results,
const unsigned char* from,
const unsigned char* to)
{
for(; from != to; from = std::next(from, 3)) {
++results[(*from << 16) | (*std::next(from, 1) << 8) | *(std::next(from,2))];
}
}
составлено с -O3
Урожайность (пояснительные комментарии включены):
__Z12count_valuesRNSt3__15arrayIjLm259EEEPKhS4_: ## @_Z12count_valuesRNSt3__15arrayIjLm259EEEPKhS4_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp0:
.cfi_def_cfa_offset 16
Ltmp1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp2:
.cfi_def_cfa_register %rbp
jmp LBB0_2
.align 4, 0x90
LBB0_1: ## %.lr.ph
## in Loop: Header=BB0_2 Depth=1
# dereference from and extend the 8-bit value to 32 bits
movzbl (%rsi), %eax
shlq $16, %rax # shift left 16
movzbl 1(%rsi), %ecx # dereference *(from+1) and extend to 32bits by padding with zeros
shlq $8, %rcx # shift left 8
orq %rax, %rcx # or into above result
movzbl 2(%rsi), %eax # dreference *(from+2) and extend to 32bits
orq %rcx, %rax # or into above result
incl (%rdi,%rax,4) # increment the correct counter
addq $3, %rsi # from += 3
LBB0_2: ## %.lr.ph
## =>This Inner Loop Header: Depth=1
cmpq %rdx, %rsi # while from != to
jne LBB0_1
## BB#3: ## %._crit_edge
popq %rbp
retq
.cfi_endproc
Обратите внимание, что нет необходимости отклоняться от стандартных конструкций или стандартных вызовов. Компилятор выдает идеальный код.
Чтобы дополнительно доказать это, давайте сходим с ума и напишем собственный итератор, который позволит нам сократить функцию до этой:
void count_values(std::array<uint32_t, 256^3>& results,
byte_triple_iterator from,
byte_triple_iterator to)
{
assert(iterators_correct(from, to));
while(from != to) {
++results[*from++];
}
}
А вот (базовая) реализация такого итератора:
struct byte_triple_iterator
{
constexpr byte_triple_iterator(const std::uint8_t* p)
: _ptr(p)
{}
std::uint32_t operator*() const noexcept {
return (*_ptr << 16) | (*std::next(_ptr, 1) << 8) | *(std::next(_ptr,2));
}
byte_triple_iterator& operator++() noexcept {
_ptr = std::next(_ptr, 3);
return *this;
}
byte_triple_iterator operator++(int) noexcept {
auto copy = *this;
_ptr = std::next(_ptr, 3);
return copy;
}
constexpr const std::uint8_t* byte_ptr() const {
return _ptr;
}
private:
friend bool operator<(const byte_triple_iterator& from, const byte_triple_iterator& to)
{
return from._ptr < to._ptr;
}
friend bool operator==(const byte_triple_iterator& from, const byte_triple_iterator& to)
{
return from._ptr == to._ptr;
}
friend bool operator!=(const byte_triple_iterator& from, const byte_triple_iterator& to)
{
return not(from == to);
}
friend std::ptrdiff_t byte_difference(const byte_triple_iterator& from, const byte_triple_iterator& to)
{
return to._ptr - from._ptr;
}
const std::uint8_t* _ptr;
};
bool iterators_correct(const byte_triple_iterator& from,
const byte_triple_iterator& to)
{
if (not(from < to))
return false;
auto dist = to.byte_ptr() - from.byte_ptr();
return dist % 3 == 0;
}
Теперь, что у нас есть?
Но что это сделало с нашим объектным кодом? (скомпилировать с -O3 -DNDEBUG
)
.globl __Z12count_valuesRNSt3__15arrayIjLm259EEE20byte_triple_iteratorS3_
.align 4, 0x90
__Z12count_valuesRNSt3__15arrayIjLm259EEE20byte_triple_iteratorS3_: ## @_Z12count_valuesRNSt3__15arrayIjLm259EEE20byte_triple_iteratorS3_
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp3:
.cfi_def_cfa_offset 16
Ltmp4:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp5:
.cfi_def_cfa_register %rbp
jmp LBB1_2
.align 4, 0x90
LBB1_1: ## %.lr.ph
## in Loop: Header=BB1_2 Depth=1
movzbl (%rsi), %eax
shlq $16, %rax
movzbl 1(%rsi), %ecx
shlq $8, %rcx
orq %rax, %rcx
movzbl 2(%rsi), %eax
orq %rcx, %rax
incl (%rdi,%rax,4)
addq $3, %rsi
LBB1_2: ## %.lr.ph
## =>This Inner Loop Header: Depth=1
cmpq %rdx, %rsi
jne LBB1_1
## BB#3: ## %._crit_edge
popq %rbp
retq
.cfi_endproc
Ответ: ничего такого — это так же эффективно.
Урок? нет действительно! Доверяй своему компилятору !!!
Предполагая, что вы хотите сделать подсчет всех различных значений (ваш код: intBuffer[number]++;
) (с intBuffer, имеющим 2 ^ 24 элемента), вы можете попробовать сделать развертывание петли:
Вместо:
for(; i<size_tot; i+=3){
uint32_t number = (bytes[i]<<16) | (bytes[i+1]<<8) | bytes[i+2];
intBuffer[number]++;
}
делать:
for(; i<size_tot; i+=12){ // add extra ckeck here..
intBuffer[(bytes[i]<<16) | (bytes[i+1]<<8) | bytes[i+2]]++;
intBuffer[(bytes[i+3]<<16) | (bytes[i+4]<<8) | bytes[i+5]]++;
intBuffer[(bytes[i+6]<<16) | (bytes[i+7]<<8) | bytes[i+8]]++;
intBuffer[(bytes[i+9]<<16) | (bytes[i+10]<<8) | bytes[i+11]]++;
}
// Add a small loop for the remaining bytes (no multiple of 12)
Это позволило бы процессору выполнить несколько инструкций за один такт (не забудьте установить оптимизацию компилятора на самом высоком уровне).
Вам также нужна дополнительная проверка для последней части bytes
,
Проверять, выписываться Инструкция по конвейерной обработке.
Инструкция по прокладке трубопровода это техника, которая реализует форму параллелизм называется параллелизмом на уровне команд внутри одного процессора. Следовательно, он обеспечивает более высокую пропускную способность процессора (количество команд, которое может быть выполнено за единицу времени), чем это было бы возможно при данной тактовой частоте.. Основной цикл инструкций разбит на серию, называемую конвейером. Вместо последовательной обработки каждой инструкции (завершение одной инструкции перед началом следующей) каждая команда разбивается на последовательность шагов так что разные шаги могут выполняться параллельно и инструкции могут обрабатываться одновременно (начиная с одной инструкции до окончания предыдущей).
Обновить:
но это невероятно медленно
На самом деле, для 3 МБ это должно быть несколько мгновенно, даже с вашим исходным кодом (учитывая, что данные уже кэшированы). Как bytes
определены? Может ли быть так operator[]
делает некоторые дополнительные проверки границ?
Прежде всего убедитесь, что оптимизация компилятора переведена на высший уровень.
Я думаю, я бы попробовал:
unsigned char* pBytes = bytes;
uint32_t number;
for(unsigned long i = 0; i<size_tot; i+=3){
number = *pBytes << 16;
++pBytes;
number = number | (*pBytes << 8);
++pBytes;
number = number | *pBytes;
++pBytes;
++intBuffer[number];
}
После компиляции я проверил бы, как выглядит созданный ассемблерный код, чтобы увидеть, действительно ли изменения изменились.
Попробуйте прочитать 4 или 8 байтов за раз, а затем объедините байты, чтобы получить желаемое значение. Является ли это быстрее или нет, нуждается в сравнительном анализе.
Это будет работать на архитектурах с прямым порядком байтов. Для байтов с прямым порядком байтов некоторая арифметика должна быть изменена, и должен использоваться обратный порядок байтов.
unsigned char *bp = bytes;
while ((uintptr_t)bp % 4) // make sure that the pointer is properly aligned
{
num = (bp[0] << 16) | (bp[1] << 8) | bp[2];
intBuffer[num]++;
bp += 3;
}
unsigned int num1, num2, num3;
unsigned int* ip = (unsigned int*)b;
while (ip+12 < bytes+size_tot)
{
num1 = *ip++;
num2 = *ip++;
num3 = *ip++;
intBuffer[num1 >> 8]++;
intBuffer[((num1 & 0xFF) << 16) | (num2 >> 16)]++;
intBuffer[((num2 & 0xFFFF) << 8) | (num3 >> 24)]++;
intBuffer[num3 & 0xFFFFFF]++;
}
bp = (unsigned char*)ip;
while (bp < bytes+size_tot)
{
num = (bp[0] << 16) | (bp[1] << 8) | bp[2];
intBuffer[num]++;
bp += 3;
}