Быстрый strlen с битовыми операциями
Я нашел этот код
int strlen_my(const char *s)
{
int len = 0;
for(;;)
{
unsigned x = *(unsigned*)s;
if((x & 0xFF) == 0) return len;
if((x & 0xFF00) == 0) return len + 1;
if((x & 0xFF0000) == 0) return len + 2;
if((x & 0xFF000000) == 0) return len + 3;
s += 4, len += 4;
}
}
Мне очень интересно знать, как это работает. ¿Может ли кто-нибудь объяснить, как это работает?
6 ответов
побитовое и с ними будет извлекать битовый шаблон из другого операнда. Значит,10101 & 11111 = 10101
. Если результат этого побитового и равен 0, то мы знаем, что другой операнд равен 0. Результат 0 при ANDing одного байта с 0xFF
(единицы) будет указывать нулевой байт.
сам код проверяет каждый байт массива char в четырехбайтовых разделах. Примечание: этот код не переносится; на другой машине или компиляторе unsigned int может быть больше 4 байты. Вероятно, было бы лучше использовать uint32_t
тип данных для обеспечения 32-разрядных целых чисел без знака.
первое, что нужно отметить, это то, что на машине с маленьким эндом байты, составляющие массив символов, будут считываться в тип данных без знака в обратном порядке; то есть, если четыре байта в текущем адресе являются битовым шаблоном, соответствующим abcd
, то беззнаковая переменная будет содержать битовый шаблон, соответствующий dcba
.
во-вторых, шестнадцатеричная константа числа в C приводит к числу int-размера с указанными байтами в конце битового шаблона. Значит,0xFF
на самом деле 0x000000FF
при компиляции с 4-байтовыми ints. 0xFF00
is 0x0000FF00
. И так далее.
таким образом, программа в основном ищет нулевой символ в четырех возможных позициях. Если нет нулевого символа в текущей секции, он переходит к следующему четырехбайтовое слот.
взять char массив abcdef
для образец. В C строковые константы всегда будут иметь нулевые Терминаторы в конце, поэтому есть 0x00
байт в конце строки.
он будет работать следующим образом:
читать "abcd" в unsigned int x:
x: 0x64636261 [ASCII representations for "dcba"]
проверьте каждый байт на наличие нулевого Терминатора:
0x64636261
& 0x000000FF
0x00000061 != 0,
0x64636261
& 0x0000FF00
0x00006200 != 0,
и проверьте две другие позиции; в этом 4-байтовом разделе нет нулевых Терминаторов, поэтому перейдите к следующему разделу.
читать "ef" в unsigned int x:
x: 0xBF006665 [ASCII representations for "fe"]
обратите внимание на байт 0xBF; это за пределами длины строки, поэтому мы читаем в мусоре из стека времени выполнения. Это может быть что угодно. На машине, которая не допускает несогласованных доступов, это приведет к сбою, если память после строки не выровнена по 1 байту. Если бы в строке остался только один символ, мы бы читали два дополнительных байта, поэтому выравнивание памяти, прилегающей к массиву символов, должно быть выровнено на 2 байта.
проверьте каждый байт для значение null:
0xBF006665
& 0x000000FF
0x00000065 != 0,
0xBF006665
& 0x0000FF00
0x00006600 != 0,
0xBF006665
& 0x00FF0000
0x00000000 == 0 !!!
Итак, мы возвращаемся len + 2
; len
было 4, так как мы увеличили его один раз на 4, поэтому мы возвращаем 6, что действительно является длиной строки.
код "работает", пытаясь прочитать 4 байта за раз, предполагая, что строка выложена и доступна как массив int
. Код читает первый int
и затем каждый байт по очереди, проверяя, является ли он нулевым символом. Теоретически, код работает с int
будет работать быстрее, чем 4 отдельныеchar
операции.
но есть несколько проблем:
выравнивание является проблемой: например *(unsigned*)s
may seg-fault.
Endian является проблемой с if((x & 0xFF) == 0)
может не получить байт по адресу s
s += 4
проблема как sizeof(int)
может отличаться от 4.
типы массивов могут превышать int
диапазон, лучше использовать size_t
.
попытка исправить эти трудности.
#include <stddef.h>
#include <stdio.h>
static inline aligned_as_int(const char *s) {
max_align_t mat; // C11
uintptr_t i = (uintptr_t) s;
return i % sizeof mat == 0;
}
size_t strlen_my(const char *s) {
size_t len = 0;
// align
while (!aligned_as_int(s)) {
if (*s == 0) return len;
s++;
len++;
}
for (;;) {
unsigned x = *(unsigned*) s;
#if UINT_MAX >> CHAR_BIT == UCHAR_MAX
if(!(x & 0xFF) || !(x & 0xFF00)) break;
s += 2, len += 2;
#elif UINT_MAX >> CHAR_BIT*3 == UCHAR_MAX
if (!(x & 0xFF) || !(x & 0xFF00) || !(x & 0xFF0000) || !(x & 0xFF000000)) break;
s += 4, len += 4;
#elif UINT_MAX >> CHAR_BIT*7 == UCHAR_MAX
if ( !(x & 0xFF) || !(x & 0xFF00)
|| !(x & 0xFF0000) || !(x & 0xFF000000)
|| !(x & 0xFF00000000) || !(x & 0xFF0000000000)
|| !(x & 0xFF000000000000) || !(x & 0xFF00000000000000)) break;
s += 8, len += 8;
#else
#error TBD code
#endif
}
while (*s++) {
len++;
}
return len;
}
он торгует неопределенным поведением (unaligned accesses, 75% вероятность доступа за пределами конца массива) для очень сомнительного ускорения (это, возможно, даже медленнее). И не соответствует стандарту, потому что он возвращает int
вместо size_t
. Даже если на платформе разрешены несогласованные доступы, они могут быть намного медленнее, чем выровненные.
он также не работает на системах big-endian, или если unsigned
не 32 бита. Не говоря уже о множественной маске и условные операции.
что сказал:
он проверяет 4 8-битных байта за раз, загружая unsigned
(который даже не гарантирован иметь больше чем 16 битов). Как только любой из байтов содержит ''
-terminator, он возвращает сумму текущей длины плюс положение этого байта. В противном случае он увеличивает текущую длину на количество байтов, проверенных параллельно (4), и получает следующее unsigned
.
мой совет: плохой пример оптимизации плюс слишком много неопределенности/подводные камни. Скорее всего, это даже не быстрее - просто профиль его против стандартной версии:
size_t strlen(restrict const char *s)
{
size_t l = 0;
while ( *s++ )
l++;
return l;
}
может быть способ использовать специальные векторные инструкции, но если вы не можете доказать, что это критическая функция, вы должны оставить это компилятору - некоторые могут развернуть/ускорить такие циклы намного лучше.
все предложения там медленнее, чем простой strlen().
причина в том, что они не уменьшают количество сравнений и только одна касается выравнивания.
Проверьте предложение strlen() от Torbjorn Granlund (tege@sics.se) и Дэн Сахлин (dan@sics.se) в сети. Если вы находитесь на 64-битной платформе, это действительно помогает ускорить работу.
Он определяет, установлены ли какие-либо биты в определенном байте на машине с небольшим концом. Поскольку мы проверяем только один байт (так как все кусочки, 0 или 0xF, удваиваются), и это случается с последней позицией байта (поскольку машина мало-конечная, и байтовый шаблон для чисел, следовательно, обращен), мы можем сразу узнать, какой байт содержит NUL.
цикл принимает 4 байта массива char для каждой итерации. Четыре оператора if используются для определения того, закончена ли строка, используя битовую маску с оператором AND для чтения состояния i-го элемента выбранной подстроки.