Строка strlen MAX 16 с использованием побитовых операторов

Задача состоит в том, чтобы найти самый быстрый способ определить в C/С++ длину c-строки, используя побитовые операции в C.

char thestring[16];

С-строка имеет максимальный размер 16 символов и находится внутри буфера Если строка равна 16 символам, в конце не имеет нулевого байта.

Я уверен, что можно сделать, но пока не понял.

Я работаю над этим на данный момент, но предполагаю, что строка memcpied в буфере с нулевым заполнением.

len =   buff[0] != 0x0 +
            buff[1] != 0x0 +
            buff[2] != 0x0 +
            buff[3] != 0x0 +
            buff[4] != 0x0 +
            buff[5] != 0x0 +
            buff[6] != 0x0 +
            buff[7] != 0x0 +
            buff[8] != 0x0 +
            buff[9] != 0x0 +
            buff[10] != 0x0 +
            buff[11] != 0x0 +
            buff[12] != 0x0 +
            buff[13] != 0x0 +
            buff[14] != 0x0 +
            buff[15] != 0x0;

Примечание: буфер с нулевым заполнением "\ 0123456789abcde" не может быть.

Ответ 1

Это будет работать отлично, поскольку buf инициализируется нулем. В вашем решении есть !=, который будет использовать инструкцию перехода. Если GPU имеет несколько блоков XOR, следующий код можно конвейерно конвейерно настроить. С другой стороны, инструкция JUMP вызовет промывку трубопровода.

len = !!buf[0] +
      !!buf[1] +
      //...
      !!buf[15]

Обновление. Приведенный выше код и код OP создают тот же код сборки при компиляции GCC с флагами -O3. (разные, если флаги оптимизации не предусмотрены)

Ответ 2

Код, который у вас есть, будет работать неправильно. Например, рассмотрим буфер, содержащий что-то вроде:

"\0123456789abcde";

В соответствии с вашим кодом это имеет длину 15, но на самом деле его длина равна 0, из-за начального "\ 0".

Как бы это ни было, чтобы делать вычисления параллельно, простой факт состоит в том, что определение строки более или менее мандатов, начиная с начала и подсчета символов, до тех пор, пока вы не встретите "\ 0", (или, в вашем случае, до 16).

Ответ 3

Вот небольшой трюк, который я читал в Hacker Delight, называемый SWAR (SIMD-in-a-register), предполагающий 8 бит на символ:

#define CHAR_BITS 8
uint_fast_16_t all_character_bits[CHAR_BITS]= { 0 };

for (int bit_index= 0; bit_index<CHAR_BITS; ++bit_index)
{
    for (int character_index= 0; character_index<16; ++character_index)
    {
        all_character_bits[bit_index]|= ((buff[character_index] >> bit_index) & 1) << character_index;
    }
}

uint_fast_32_t zero_byte_character_mask= ~0;

for (int bit_index= 0; bit_index<CHAR_BITS; ++bit_index)
{
    zero_byte_character_mask&= (0xffff0000 | ~all_character_bits[bit_index]);
}

uint_fast_8_t first_null_byte= first_bit_set(zero_byte_character_mask);

где first_bit_set - любое количество популярных и быстрых реализаций поиска первого бита, установленного в целое число.

Основная идея здесь состоит в том, чтобы взять 16 символов как матрицу размером 8x16 и AND побитовое NOT всех столбцов вместе. Любая строка, имеющая все нули, будет иметь бит этой строки, установленный в результате. Затем мы просто находим первый бит, установленный в результате, и длину строки. Эта конкретная реализация гарантирует, что биты 16-31 задаются в результате, если все символы не являются NULL. Фактическая битовая транспозиция может быть намного быстрее (что означает без ветвей).

Ответ 4

Побитовые операции... может быть, что-то вроде:

// TODO: optimize for 64-bit architectures
uint32_t *a = (uint32_t*)thestring;

for (int i = 0; i < 4; i++) // will be unwound
    for (int j = 0; j < 4; j++)
        if (a[i] & 0xff << j == 0)
           return 4*i+j;
return 16;

Ответ 5

Вы можете начать с

template <typename T>
bool containsANull(T n) {
   return (n  - ((T) -1)/255) & ((T) -1)/255*128) & ~n;
}

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

Как это работает?

(T) -1/255 - это битовая диаграмма 0x01010101, повторяющаяся до тех пор, пока это требуется

(T) -1/255 * 128, таким образом, повторяется битовая диаграмма 0x80808080

if n is                        0x0123456789ABCDEF
n - 0x1111..1 is               0xF0123456789ABCDE
(n-0x1111...1) & 0x8888...8 is 0x8000000008888888
~n is                          0xFEDCBA9876543210 
so the result is               0x8000000000000000

Единственный способ получить не-нулевой байт здесь - начать с нулевого байта.

Ответ 6

Пожалуйста, обратитесь к fstrlen(), реализованному Полом Се в...

http://www.azillionmonkeys.com/qed/asmexample.html

Хотя это не совсем то, что вы ищете, с небольшой настройкой он должен сделать это за вас.

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

Ответ 7

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

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

set R1, 0
test R2+0, 0
cinc R1                   ; conditional increment
test R2+1, 0
cinc R1
...

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

Если компилятор выполнил отличное задание, на многих процессорах это будет выглядеть примерно так:

set R1, 0
test R2+0, 0
jz end  ; jump if zero
inc R1
test R2+1, 0
jz end
inc R1
...

Это также может быть приемлемым, если не сопровождаемые условные прыжки не причинят вам вреда, так как после этого у вас есть только один условный переход (первый, где вы найдете 0).

Поскольку вы сказали, что настроили таргетинг на графический процессор, и те, которые, как правило, очень дружелюбны к математике, вы могли бы сделать:

int acc = 0;
acc += str[0]/str[0];
acc += str[1]/str[1];
...

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

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

Вы должны проверить Бит Twiddling Hacks для крутого способа ускорения strlen, который хорошо работает для больших размеров регистра.

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

Ответ 8

В гипотетическом языке на С++, предполагающем 2 дополнения и мало-endian,

int128_t v = *reinterpret_cast<int128_t*>(thestring);
const int bit_count = 128;
int eight = ((1 << 64) - 1 - v) >> (bit_count - 4) & 8;
v >>>= 8 * eight;
int four  = ((1 << 32) - 1 - v) >> (bit_count - 3) & 4;
v >>>= 8 * four;
int two   = ((1 << 16) - 1 - v) >> (bit_count - 2) & 2;
v >>>= 8 * two;
int one   = ((1 <<  8) - 1 - v) >> (bit_count - 1) & 1;
return (one | two | four | eight) + !!v;

(Изменено из http://graphics.stanford.edu/~seander/bithacks.html#IntegerLog.)

Ответ 9

Вы можете бить все, что хотите, но вы, вероятно, не побьете это:

int fast1(const char *s)
{ 
    if (!*s++) return 0; 
    if (!*s++) return 1; 
    if (!*s++) return 2; 
    if (!*s++) return 3; 
    if (!*s++) return 4; 
    if (!*s++) return 5; 
    if (!*s++) return 6; 
    if (!*s++) return 7; 
    if (!*s++) return 8; 
    if (!*s++) return 9; 
    if (!*s++) return 10; 
    if (!*s++) return 11; 
    if (!*s++) return 12; 
    if (!*s++) return 13; 
    if (!*s++) return 14; 
    if (!*s++) return 15; 
}

В качестве альтернативы вы можете сделать это: (будет ли это быстрее зависит от вашего процессора и компилятора).

int fast2(const char *s)
{ 
    if (!s[0]) return 0; 
    if (!s[1]) return 1; 
    if (!s[2]) return 2; 
    if (!s[3]) return 3; 
    if (!s[4]) return 4; 
    if (!s[5]) return 5; 
    if (!s[6]) return 6; 
    if (!s[7]) return 7; 
    if (!s[8]) return 8; 
    if (!s[9]) return 9; 
    if (!s[10]) return 10; 
    if (!s[11]) return 11; 
    if (!s[12]) return 12; 
    if (!s[13]) return 13; 
    if (!s[14]) return 14; 
    if (!s[15]) return 15; 
}

Update:

Я профилировал обе эти функции на моем Core2Duo T7200 @2.0 ГГц, Windows XP pro, Visual Studio 2008 с отключенными оптимизациями. (Включение оптимизатора заставляет VS заметить, что в моем цикле синхронизации нет выхода, поэтому он полностью удаляет его).

Я вызывал каждую функцию в цикле 2 22 раз, затем принимал среднее значение более 8 прогонов.

fast1 занимает около 87,20 нс на вызов функции.

fast2 занимает около 45,46 нс на вызов функции.

Итак, на моем процессоре версия индексации массива почти в два раза быстрее, чем версия указателя.

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

Обновление 2

Эта функция довольно быстро, примерно 60 нс за звонок. Я предполагаю, что разыменование указателя выполняется блоком адреса и умножением на целую единицу, поэтому операции конвейерируются. В моих других примерах вся работа выполняется с помощью адресной единицы.

int fast5(const char *s)
{
    return  /* 0 * (s[0] == 0) + don't need to test 1st byte */
            1 * (s[1] == 0)  +
            2 * (s[2] == 0)  +
            3 * (s[3] == 0)  +
            4 * (s[4] == 0)  +
            5 * (s[5] == 0)  +
            6 * (s[6] == 0)  +
            7 * (s[7] == 0)  +
            8 * (s[8] == 0)  +
            9 * (s[9] == 0)  +
            10 * (s[10] == 0) +
            11 * (s[11] == 0) +
            12 * (s[12] == 0) +
            13 * (s[13] == 0) +
            14 * (s[14] == 0) +
            15 * (s[15] == 0);
}

Ответ 10

Предполагая 64-битную и малочисленную систему:

long a = ((long *)string)[0];
long b = ((long *)string)[1];

a = (a - 0x0101010101010101UL) & ~a & 0x8080808080808080UL;
b = (b - 0x0101010101010101UL) & ~b & 0x8080808080808080UL;

return a ? count_trailing_zeros( a ) / 8 : b ? 8 + count_trailing_zeros( b ) / 8 : 16;

Для больших энтианских отсчетов ведущие нули. Любая система strlen-реализация будет использовать это.