Быстрый 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-го элемента выбранной подстроки.