2016-06-28 3 views
1

я нашел this «быстро StrLen функции» реализации:Быстрая StrLen функции против правил наложения спектров

// for x86 only 
size_t my_strlen(const char *s) { 
    size_t 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; 
    } 
} 

Оптимизация techique используется здесь, очевидно, просто: чтения памяти естественных словами CPU (код старый и предполагает x32 CPU), а не просто байтами.

Но этот код нарушает правила псевдонимов и поэтому вызывает неопределенное поведение, которое может быть свободно оптимизировано компилятором (они делают код еще быстрее, но inccorect).

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

Или может быть, я совершенно неправильно здесь, а приведенный выше код верен? Правильно ли это для C? Для C++?

+1

Это нарушает правило. Но попытались ли вы сравнить его? Я уверен, что компилятор будет лучше работать, оптимизируя стандартный 'strlen'. –

+1

Он также нарушает выравнивание. –

+4

[OT] Если вы используете 'std :: string' вместо' char * ', то функция' size' - это O (1), которую трудно превзойти :) – NathanOliver

ответ

5

Это очень плохой код. Автор даже Кодекса предупреждает:

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

  • Эти слова могут быть неравнозначными, но архитектура x86 позволяет получить доступ к не выровненным данным. Для небольших строк, , выравнивание займет больше времени, чем штраф за неудовлетворенное чтение.

  • Код не переносится: при использовании 64-разрядного процессора вам придется добавить еще 4 условия. Для архитектуры большого конца порядок условий должен быть отменен.

Даже если это не нарушали правила сглаживания нагрузка на кодере, чтобы сделать работу my_strlen совершенно неоправданная. Несколько раз было указано, что strlen уже будет настроен за пределы того, что может достичь средний кодер.

Но дополнительное заявление должно быть сделано для C++: Bjarne Stroustrup, the creator of C++, в last page of chapter 4 in his book: "А ++ Язык программирования C" говорит:

Предпочитает strings над строками в стиле С

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

EDIT:

В your comment вы говорите, что вы работаете с StaticallyBufferedString, который имеет целью решить "пулы модель памяти" string «s, который вызывает:

  • Ненужные кучи замков в многопоточном контексте
  • Фрагментация от контроля в реальном времени

Я хотел бы предложить C++ 17's string_view, который, как и весь C++ 17, был построен с многопоточным разумом. Он обеспечивает функциональность string с поддержкой кучи и constexpr дружественных C-строк. Вы даже можете начать сканирование, узнав об этом с помощью namespace experimental: http://en.cppreference.com/w/cpp/experimental/basic_string_view В отличие от вашего времени, установленного на StaticBufferedStrings, полученные вами знания будут совершенно переносимы и применимы к любой будущей работе на C++, которую вы делаете!

+1

Спасибо за ваш комментарий! ВСЕ из нас, через, написал плохой и даже очень плохой код, за что нам должно быть стыдно, поэтому я не буду судить автора слишком тяжелым;) –

+1

@AmaboCarab Вы делаете справедливую точку. Если вы посмотрите на мою историю вопроса здесь, я продемонстрировал больше, чем несколько недоразумений, которые пробились в старый код, который я написал. Поэтому я отредактировал, чтобы удалить мои каустические комментарии, которые были направлены на автора. –

+1

действительно мудрое решение. Способность понимать наши недостатки, признавать и исправлять их, учиться на наших ошибках, состоит в том, что на самом деле отделяет умных людей от глупых :) –

4

Это плохая идея, и это ухудшит вашу производительность. Стандартная функция strlen, предоставляемая современными компиляторами, уже оптимизирована и будет работать намного лучше, чем указано выше. Например, на процессорах с поддержкой SSE (т. Е. Почти во всех них) он уже будет использовать инструкции SSE/AVX для векторизованного поиска нулевого терминатора и будет рассматривать более 4 байтов одновременно, как указано выше, с меньшим количеством сравнения - и меньше ветвей, которые могут быть неправильно предсказаны.

+1

С веб-страницы: ** Эта статья должна рассматриваться как упражнение в оптимизации кода, а не как рекомендация для повседневного программирования. ** Она оптимизирована по сравнению с наивной реализацией C 'strlen()', а не той, которая встроена в библиотека. – Barmar

+0

@Smeeheey, вы абсолютно правы, да, конечно, современный компилятор будет просто использовать встроенный bulltin (если он включен параметрами компилятора), но вопрос был о правильности кода, потому что, как я вижу его сейчас, он нарушает сглаживание, выравнивание и endianess. –

+0

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

1

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

float *fp; 
int i; 

int foo(void) 
{ 
    i++; 
    *fp=1.0f; 
    return i; 
} 

от того, чтобы пессимистически предположить, что запись в *fp может повлиять на i, несмотря на полное отсутствие чего-либо, что можно было бы предположить, что что-то типа int может быть затронуто. Код, который использовал тип punning для различных целей, включая оптимизацию каналов, был широко распространен, когда был написан C89, но в большинстве случаев такой код включал бы четкие указания компилятору, что будет происходить сглаживание. Как правило, если объект будет изменен указатель иностранного типа между двумя «нормальный» доступ, один или оба будут происходить между двумя нормальными доступов следующее:

  1. адрес объекта будут приняты ,

  2. Указатель будет преобразован из типа объекта в другой тип.

Помимо очевидного случая, когда указатель используется для доступа к объекту из его точного типа, стандарт в основном определяет случаи, когда это не было бы очевидно, что компилятор должен предполагать наложение спектров было возможно (например, между «int» и указатель типа «unsigned *», или между чем-либо и указателем типа «char *»). Учитывая обоснованность, я думаю, что авторы намеревались сосредоточиться на том, чтобы авторы сценариев обрабатывали случаи, когда не было бы основанием ожидать наложения псевдонимов, но не думали, что они должны быть рассказали, как идентифицировать случаи, когда это было очевидно, вероятно.

отрывов оптимизации будут безопасными на компиляторы, которые признают, что Адресов и литейные операторов предполагают, что перекрестный тип сглаживание, вероятно, при условии, что любое использование указателя в результате броска выполняется до следующего доступа с использованием указателя без заливки - требование, которое обычно соответствует большинству . К сожалению, нет стандарта для «sane-compiler C», а gcc использует тот факт, что авторы стандарта не требовали, чтобы компиляторы обрабатывали случаи очевидного сглаживания как оправдание , чтобы игнорировать их.

Тем не менее, преимущество производительности от отрывов оптимизации может перевесить затраты производительности -fno-strict-aliasing, особенно если код использует restrict классификаторов, когда это необходимо. Есть несколько ситуаций, в основном с глобальными переменными, где restrict не является достаточным для включения полезных оптимизаций; они могут обрабатываться режимом псевдонимов, который ограничивает анализ объектами статической или автоматической продолжительности (например, объекты в примере обоснования ), но gcc не предлагает такого режима.

Кстати, я не уверен, что тайминги инструкции, как на современных процессорах x86, но на некоторых ARM варианты компиляторы будут иметь шанс получения оптимального кода долго струнами из чего-то вроде:

uint32_t x01000000 = 0x01000000; 
uint64_t *search(uint64_t *p) 
{ 
    uint64_t n; 
    uint32_t a,b; 
    uint32_t v = x01000000; // See note 
    do 
    { 
    n=*p++; 
    a=(uint32_t)n; 
    b=n>>32; 
    if (a >= v || (a << 8) >= v || (a << 16) >= v || (a << 24) >= v || 
     b >= v || (b << 8) >= v || (b << 16) >= v || (b << 24) >= v) return p; 
    } while(1);  
} 

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

Многие процессоры ARM имеют инструкцию, которая может сравнивать сдвинутое значение с регистром; компиляторам иногда нужно немного помочь понять, что 0x01000000 следует хранить в регистре (существует команда сравнения с константой, но не включает «свободный» сдвиг сравниваемого регистра), но с помощью они могут найти сравнение со сдвигом. Я еще не нашел способ убедить компилятор для генерации оптимального кода для ARM7-TDMI, которое было бы эквивалентно:

search: 
    mov r1,#0x010000000 
lp: 
    ldrmia r0,{r2,r3} 
    cmp r1,r2 
    cmplt r1,r2,asl #8 
    cmplt r1,r2,asl #16 
    cmplt r1,r2,asl #24 
    cmplt r1,r3 
    cmplt r1,r3,asl #8 
    cmplt r1,r3,asl #16 
    cmplt r1,r3,asl #24 
    blt lp 
    bx  lr 

Это займет 15 циклов за восемь байт; он может быть адаптирован для выполнения 25 циклов на шестнадцать байт. Цикл, который обрабатывает восемь байтов индивидуально, займет 42 цикла; разворачивается до шестнадцати байтов, это будет 82 цикла. Лучшая петля, которую я видел, компиляторы генерируют для кода на основе uint64_t, будет 22 цикла для восьми байтов - почти наполовину до тех пор, пока оптимальный код, но все же примерно в два раза быстрее, чем версия с байтами.