Как можно улучшить эти функции преобразования регистра?

В качестве обучающего упражнения каждая из трех моих функций - ToggleCase, LowerCase и UpperCase - ожидает указатель на строку символов ASCII, оканчивающуюся нулевым символом; они работают как положено. Есть ли более эффективные или более быстрые методы решения этой задачи? Нарушаю ли я какие-то невысказанные правила хорошего программирования на C? Я использовал макросы, потому что, как мне кажется, они делают код лучше и более эффективны, чем вызовы функций. Это типично или чрезмерно?

Пожалуйста, не стесняйтесь придираться и критиковать код (но будьте любезны).

case_conversion.h

#define CASE_FLAG 32
#define a_z(c) (c >= 'a' && c <= 'z')
#define A_Z(c) (c >= 'A' && c <= 'Z')

void ToggleCase(char* c);
void LowerCase(char* c);
void UpperCase(char* c);

case_conversion.c

#include "case_conversion.h"

void ToggleCase(char* c)
{
 while (*c)
 {
  *c ^= a_z(*c) || A_Z(*c) ? CASE_FLAG : 0;
  c++;
 }
}
void LowerCase(char* c)
{
 while (*c)
 {
  *c ^= A_Z(*c) ? CASE_FLAG : 0;
  c++;
 }
}
void UpperCase(char* c)
{
 while (*c)
 {
  *c ^= a_z(*c) ? CASE_FLAG : 0;
  c++;
 }
}

person RLH    schedule 07.10.2010    source источник
comment
@aaa: Похоже, он выполняет XOR, чтобы переключить регистр.   -  person andand    schedule 07.10.2010
comment
Таблица поиска будет быстрее. Кроме того, возможно, что for (int i = strlen(s); i; i--) будет быстрее, чем while (*c) (по крайней мере, в тех случаях, когда strlen оптимизирован).   -  person ruslik    schedule 07.10.2010
comment
#define ToggleCase (str) char s_ = str; while (* s_) (* s _ ++) = ((* s _ ›= 'A') && (* s _‹ = 'Z'))? (* s_ + 32): (((* s _ ›= 'a') && (* s _‹ = 'z'))? (* s_-32): * s_) (странно, он теряет '', где s _ ++ встречается?)   -  person slashmais    schedule 07.10.2010
comment
@slashmais Да, я мог бы сделать длинное #define заявление, которое подтвердит то, что вы опубликовали. Однако это не очень читается. При этом использование символа разрыва строки макроса / сделает ваш код более читабельным. Я поиграю с этим и посмотрю, что я думаю.   -  person RLH    schedule 07.10.2010
comment
@RLH: эй! это было то, что развлекались, но если вы можете извлечь из этого выгоду, все к лучшему!   -  person slashmais    schedule 07.10.2010
comment
@slashmais: он теряет *, потому что думает, что это разметка курсивом (я не знаю точных правил парсера - с некоторыми так обращаются, с другими - нет). Используйте обратные кавычки (`) вокруг кода в комментариях. Он не просто помещает его в шрифт фиксированной ширины, он также отключает разметку.   -  person Steve Jessop    schedule 07.10.2010
comment
@slashmais Я только что комментировал ваш ответ. Я как бы думал вслух ради полного раскрытия информации. Спасибо за комментарий.   -  person RLH    schedule 07.10.2010
comment
FWIW, ваше использование условных выражений, логических логических операторов и условного тернара, вероятно, генерирует несколько ветвей. Я вполне уверен, что если бы вы создали вариант без ветвей, который генерировал CASE_FLAG из выражения без ветвей (без условных выражений и без логического выражения), это было бы значительно быстрее. Фактически, просто создание таблицы поиска из 256 записей с флагами islowercase / isuppercase в ней также будет быстрее. Будет ли версия без ветвлений или версия поиска самой быстрой, зависит от того, насколько быстро вы можете вычислить и использовать предикаты,   -  person Adisak    schedule 07.10.2010
comment
@ruslik - каким образом вы бы оптимизировали strlen () так, чтобы это было быстрее, чем простое увеличение указателя? особенно потому, что вам все равно нужно увеличивать указатель (или выполнять индексированное разыменование)?   -  person Anon    schedule 07.10.2010
comment
посмотрите, как работают ctype.h / isprint / isdigit и т. д. это (обычно) v умный поиск в таблице   -  person pm100    schedule 08.10.2010
comment
@Anon strlen() от CRT оптимизирован, и это быстрее, чем простая реализация. Он проверяет больше символов за один шаг.   -  person ruslik    schedule 08.10.2010
comment
Вы предполагаете, что между прописными и строчными буквами есть разница в один бит. Это не относится к каждому набору символов. Также предположение, что английский - единственный язык, который вам нужно поддерживать, немного нечувствителен к культурным особенностям.   -  person Martin York    schedule 08.10.2010
comment
Поскольку возникла тема без ветвей, еще одним хорошим упражнением, включающим исключение ветвей, было бы написать двоичный поиск без веток. Это один из моих любимых инструментов в моем наборе инструментов.   -  person R.. GitHub STOP HELPING ICE    schedule 08.10.2010
comment
@Martin: Действительно, функция преобразования регистра ASCII в наши дни почти бесполезна, но я бы сказал, что она ориентирована не на английский, а на компьютерный язык. В наши дни такие функции даже не очень полезны для английского языка из-за угловых регистров и наличия символов, отличных от ASCII, но они по-прежнему очень полезны для работы с уродливыми текстовыми форматами, основанными на ASCII без учета регистра, такими как HTML.   -  person R.. GitHub STOP HELPING ICE    schedule 08.10.2010
comment
Согласен с Мартином Йорком. Кроме того, таблица поиска может быть создана для работы с UTF-8, если ваша инструментальная цепочка чиста от UTF-8. (Как кажется, GCC.)   -  person Prof. Falken    schedule 08.10.2010
comment
Действительно незначительная придирка: я бы предпочел is_upper / IsUpper / IS_UPPER (и т.д., согласно вашему соглашению) вместо A_Z, и аналогично для a_z. Возможно, даже is_upper_ascii.   -  person    schedule 08.10.2010
comment
@ruslik - все хорошо, но подумайте, что на самом деле делает OP: перебирает строку и проверяет каждый символ. Эта задача не изменится. Так что, если вы можете сказать мне, как добавление strlen() в микс каким-то образом ускорит его - кроме того, что я слышал, он оптимизирован - я дам вам золотую звезду.   -  person Anon    schedule 08.10.2010
comment
@Anon, пожалуйста: graphics.stanford.edu/~seander/bithacks.html# ZeroInWord При таком подходе у вас есть одна ветвь на каждое слово, а не на байт (на современных суперскалярных процессорах ветвление очень дорогое).   -  person ruslik    schedule 08.10.2010


Ответы (12)


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

a_z('a' + 1);

Вызов не даст правильных результатов из-за приоритета оператора. Это легко исправить с помощью скобок:

#define a_z(c) ((c) >= 'a' && (c) <= 'z')

Но их также можно назвать так:

a_z(i++);

Этот вызов увеличит i вдвое! И это нелегко исправить (если вообще) в макросе. Вместо этого я бы рекомендовал использовать встроенные функции (при необходимости - см. Ниже).

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

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

char toUpper[128]; // we care only about standard ASCII
for (int i = 0; i < 128; i++)
  toUpper[i] = i;
toUpper['a'] = 'A';
...
toUpper['z'] = 'Z';

И преобразование тривиально:

char toUpperCase(char c)
{
  return toUpper[c];
}

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

person Péter Török    schedule 07.10.2010
comment
@Peter Torok - Ах, я забыл об идее справочной таблицы. Это было бы быстро. Кроме того, я изменю свой код и «вставлю» случаи ошибок, как вы упомянули, для экспериментов. Спасибо за комментарии. Все они весьма полезны. - person RLH; 07.10.2010
comment
для встроенных систем таблицы поиска могут быть слишком большими. - person Anycorn; 07.10.2010
comment
@aaa, только что добавил примечание об этом, пока вы комментировали :-) - person Péter Török; 07.10.2010
comment
проверить меня take 'char slot [] = {0, 31, 63, 63}; * c = слот [* c / 32] + * c% 32; '. это почти работает :-) - person Anycorn; 07.10.2010
comment
Вопрос задан по оптимальной реализации. Я думаю, что branchfree превзойдет поиск по таблице в longrun, потому что branchfree может обрабатывать несколько символов одновременно с использованием SWAR / SIMD, а это нецелесообразно с таблицами, потому что размер таблицы растет экспоненциально на каждый обработанный символ. - person Adisak; 07.10.2010
comment
@RLH - часто можно использовать таблицы поиска. Однако они занимают место, особенно в кэше процессора (который может быть довольно маленьким во встроенной системе), и требуют индексированного разыменования. if, который не разветвляется в общем случае, на самом деле может быть быстрее, если вы знаете, что ваши строки в основном символы ... что является еще одним способом сказать, не оптимизируйте, если вы не профилируете. - person Anon; 07.10.2010

Мои любимые:

*c += (*c-'A'<26U)<<5; /* lowercase */
*c -= (*c-'a'<26U)<<5; /* uppercase */
*c ^= ((*c|32U)-'a'<26)<<5; /* toggle case */

Поскольку вашей целью будут встроенные системы, вы должны научиться устранять ненужное раздувание кода, ветвления и т. Д. Условием для определения того, является ли символ ascii алфавитным, являются 4 операции сравнения / ветвления; у меня 1. Я бы порекомендовал поискать хорошие ресурсы по трюкам с арифметикой и битовыми манипуляциями.

Примечание. Я изменил *32 операции на <<5 после публикации своего ответа, потому что ряд компиляторов встроенных систем слишком плох, чтобы сделать это за вас. При написании кода для хорошего компилятора *32, вероятно, лучше проиллюстрирует ваши намерения.

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

  1. Загрузить *c и расширить его нулем или знаком, чтобы заполнить слово размером int (в зависимости от того, является ли простой char знаковым или беззнаковым).
  2. Вычтите константу 26 с помощью беззнаковой (без перегрузки) инструкции sub.
  3. Условный переход за остальную часть кода, если флаг переноса не установлен.
  4. В противном случае добавьте 32 к значению *c.

Шаги 2 и 3 могут быть объединены в архитектурах, которые используют операцию сравнения-перехода вместо флагов. Единственный способ увидеть, как возникают какие-либо значительные скрытые затраты, - это если машина не может напрямую адресовать символы или если она использует неприятное (знак / величина или дополнение) представление значения со знаком, и в этом случае преобразование в беззнаковое было бы нетривиально. Насколько мне известно, ни одна современная встраиваемая архитектура не имеет этих проблем; они в основном изолированы от устаревших мэйнфреймов (и, в меньшей степени, от DSP).

Если кого-то беспокоят плохие компиляторы, фактически выполняющие арифметические операции для <<5, вы можете попробовать:

if (*c-'A'<26U) *c+=32;

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

Редактировать 2: По запросу, версия первой строки без ответвлений:

*c += (64U-*c & *c-91U)>>(CHAR_BIT*sizeof(unsigned)-5);

*c += (64U-*c & *c-91U) >> CHAR_BIT*sizeof(unsigned)-1 << 5;

Чтобы это работало надежно, c должен иметь тип unsigned char *, а unsigned int должен быть строго шире, чем unsigned char.

person R.. GitHub STOP HELPING ICE    schedule 07.10.2010
comment
Обратите внимание, что хотя у вас нет явных ветвей в коде, многие компиляторы будут генерировать ветки из-за операции '‹'. Кроме того, сгенерированное компилятором логическое преобразование в целое число иногда является медленной операцией. Ваш метод изящен в коде для использования простого сравнения без знака для проверки диапазона (он заставляет подписанные символы становиться беззнаковыми целыми числами). Однако в нем есть много неявных операций, сгенерированных компилятором, которые, вероятно, будут медленнее, чем явная версия без ветвей, без операций сравнения и без преобразования типа boolean-int. - person Adisak; 07.10.2010
comment
Престижность «Плюс 1» за использование «| 32» для проверки одного диапазона для переключения регистра. Я определенно украду эту идею, если когда-нибудь напишу вариант переключения без ветвей. - person Adisak; 07.10.2010
comment
@Adisak: Как я уже сказал, моя версия включает одну ветку. Вы также можете устранить это, используя причудливую смену битов, поскольку вы знаете возможный диапазон значений *c, но я думаю, что на большинстве платформ это будет дороже. - person R.. GitHub STOP HELPING ICE; 07.10.2010
comment
@R: Я работаю над видеоиграми. Платформы, на которых мы работаем, имеют значительные штрафы за неправильное предсказание веток. Фактически, даже такие CPU, как ATOM, имеют значительные штрафы за неправильное предсказание переходов. Одно неверное предсказание может стоить столько же времени, сколько преобразование нескольких символов без неверных предсказаний. Кроме того, чтобы выполнить SIMD / SWAR (обрабатывать несколько символов одновременно для самых быстрых результатов), вам необходимо удалить ветвь. - person Adisak; 07.10.2010
comment
@R: Кроме того, даже без методов SIMD / SWAR, версию без ветвей можно развернуть и чередовать, чтобы скрыть задержки, что приведет к более быстрой версии. - person Adisak; 07.10.2010
comment
В вашей версии без веток есть пара ошибок. # 1) маска равна 0x00 или 0x1f (не 0x20), поэтому 'B' - ›'a'. Если вы используете подписанные значения int для вычитаний, вы можете генерировать произвольные значения смещения отсюда by и ops, такие как '& 32' или '& (-32) '. Вы можете использовать unsigned и shift less перед '& 32', но это не может распространяться на генерацию произвольных масок, таких как '& (- 32)'. Однако после добавления операции и вы используете тот же алгоритм, что и мой ответ без ветвей. # 2) Диапазон от 'A' до 'z' (включает [] ^ _ `), и я почти уверен, что вы хотите, чтобы этот ToLower заканчивался на 'Z', поскольку он не работает для символов или a-z - person Adisak; 07.10.2010
comment
Любой компилятор, который не может оптимизировать * 32 для сдвига на пять, вероятно, выдаст что-то действительно неприятное, взяв результат условного выражения и сдвинув его. Лучше было бы что-то вроде c ^ = (((unsigned char) (ch-'A ') ›= 26) -1) & 0x20; - person supercat; 07.10.2010
comment
В написанной мной версии без ветвей есть два вычитания, два и и сдвиг для вычисления добавленной стоимости. В приведенной выше версии без ответвлений на одну операцию меньше и, но, к сожалению, она не работает, и для ее исправления требуется добавить еще одну операцию 'and' (и немного изменить сдвиг). Это делает ее эквивалентной в работе однострочной версии моей функции, которая просто расширена для ясности. - person Adisak; 08.10.2010
comment
@supercat: в нем все еще есть ветки. Извините, я действительно рекомендую использовать без ветвей как самый быстрый ответ - это единственный вариант, который невозможно прокрутить или использовать SIMD. - person Adisak; 08.10.2010
comment
@Adisak: В течение нескольких секунд после того, как я опубликовал его, в моем ответе случайно было значение ASCII 'z'+1 вместо 'Z'+1, но я быстро это исправил. В моем коде нет маски, поэтому я не понимаю, что вы говорите неправильно с какой-либо маской. Он просто вычитает старшие биты в каждом направлении и сдвигает их вниз в бит регистра ASCII. - person R.. GitHub STOP HELPING ICE; 08.10.2010
comment
@R: По-прежнему есть ошибка в генерации маски (где маска является селектором без ветвлений перед добавлением значения смещения). С вашим текущим сообщением: 'Mask = (64U- c & * c-91U) ›› (CHAR_BIT sizeof (unsigned) -5)' - значения, сгенерированные здесь для Mask, по-прежнему равны 0x00 и 0x1f что дает постепенную ошибку. Следовательно, ToLower ('B') возвращает 'a', используя ваш алгоритм. Есть несколько способов исправить алгоритм, но все они требуют добавления одной операции. Я собираюсь расширить свой ответ, чтобы маска была более очевидной. - person Adisak; 08.10.2010
comment
Исправлено использование одного из них после того, как не удалось найти никаких способов сделать это без добавления операции. - person R.. GitHub STOP HELPING ICE; 08.10.2010
comment
@R ..: Да, я почти уверен, что в написанной мной версии было минимальное количество операций для без ветвей. Вот почему я перепроверил вашу, когда у нее на один меньше. Если найдете более быструю версию, дайте мне знать. FWIW, я по-прежнему предпочитаю использовать 'и', а не дополнительный сдвиг, потому что из-за затрат на баррель-шифтер некоторые платформы позволяют выдавать инструкции сдвига только из определенного слота конвейера (например, Atom), что делает чередующиеся развернутые версии (генерируемые некоторыми компиляторы) с большей вероятностью будут зависать. - person Adisak; 08.10.2010
comment
@Adisak: у некоторых процессоров есть инструкция, которая дает 0 или 1 в зависимости от результатов сравнения; моя версия отлично с этим справилась бы. В противном случае, для отсутствия ветвлений, возможно, ch ^ = ((((unsigned char) (ch-'A ') - 26) ›› 8) & 0x20). Эффективность этого будет зависеть от того, как процессор обрабатывает приведение. Объединение с 255 может быть более эффективным, чем преобразование. - person supercat; 08.10.2010
comment
@supercat: Да, некоторые процессоры имеют инструкции по генерации предикатов, а некоторые также имеют условный выбор. Даже современный набор инструкций X86 был расширен за счет включения некоторых из этих инструкций. Однако ни один из распространенных компиляторов не использует их :( Для большинства из них вам нужно написать asm. Суть написания кода без ветвей заключается в том, что он будет генерировать код без ветвей, даже если ваш компилятор недостаточно умен, чтобы сделать это за вас. Это необходимо компилятору для автоматического разворачивания и чередования циклов.Также необходимо использовать этот метод для расширения до SIMD. - person Adisak; 08.10.2010
comment
@supercat: если кто-то спрашивает об оптимальной функции, мне никогда не комфортно оставлять фактическую производительность в руках возможной волшебной оптимизации компилятора, которая может произойти на некоторых процессорах - особенно когда я заметил, что компиляторы обычно не выполняют этого оптимизация для вас. - person Adisak; 08.10.2010
comment
@Adisak: Как разработчик встраиваемых систем, я часто имел удовольствие писать действительно глупый код, чтобы обойти глупости компилятора. К сожалению, отвратительно выглядящая перезапись, которая прекрасно работает с одной версией компилятора, может привести к ужасному коду в другой. На некоторых машинах код, использующий смешанные типы данных, будет работать нормально; на других не будет. - person supercat; 09.10.2010
comment
@supercat: Кстати, версия без веток, указанная в вашем ответе, работать не будет. Выражение до сдвига - это беззнаковый символ, а ›› 8 приводит к 0 для всех беззнаковых символов. - person Adisak; 09.10.2010
comment
@supercat: Ответ, который я даю, не является глупым, он довольно стандартен для техник C / C ++ без ветвей и сгенерирует довольно хороший код на любом современном компиляторе. Конечно, есть моменты, когда нужно использовать код ветвления, а не без ветвлений ... например, на очень старомодном неконвейерном ЦП, который не имеет предварительной выборки и штрафа за неправильное прогнозирование ветвления, а также не имеет переключателя ствола - в этом случае ветвление всегда будет Быстрее. Всегда лучше рассчитывать время и смотреть на вывод компилятора, но я обнаружил, что код без ветвей в целом работает очень быстро. - person Adisak; 09.10.2010
comment
@Adisak: Ваша критика кода supercat неверна. Левая сторона >>8 - это не unsigned char, а int из-за рекламных акций по умолчанию. В C. нет такой вещи, как выражение типа unsigned char. - person R.. GitHub STOP HELPING ICE; 09.10.2010
comment
@Adisak: Я не говорил, что ваш код глупый. Я сказал, что некоторые компиляторы глупы. Если процессор включает инструкцию, которая дает единицу, если флаг установлен, и ноль, если он очищен, и если эта инструкция быстрее, чем код ветвления, я бы подумал, что разумный компилятор должен использовать ее вместо ветвления. Есть ли веская причина, по которой компилятор не должен этого делать? - person supercat; 09.10.2010
comment
@Adisak: Кстати, ряд процессоров, таких как ARM, включают возможность выборочного пропуска инструкций. Выражение, подобное данному, может быть выполнено как безусловное вычитание A из временного регистра, вычитание при обновлении флагов 26 и XOR-if-перенос исходного регистра с 32. Три инструкции; три цикла. Я не знаю, найдет ли компилятор эту кодовую последовательность, но она довольно хороша. Если в два регистра предварительно загружены константы, обработка слов займет 9 циклов / 4 байта, хотя компилятор не найдет такой подход. - person supercat; 09.10.2010
comment
@supercat: Теоретически нет причин, по которым компилятор не должен выполнять эту оптимизацию. Однако на практике большинство компиляторов, которые я использовал, не работают :-( Чтобы избежать разрыва между теорией и практикой, если вам нужен код без ветвей в реальной практике (а не в теории), вам нужно написать его так, чтобы компилятор МОЖЕТ НЕ генерировать ветку - не писать ее, чтобы компилятор мог оптимизировать ветку. Без ветвей - огромный выигрыш в оптимизации этого кода. Если вы посмотрите на мой пост, у меня действительно есть моментальный снимок вывода asm из компилятор, показывающий, насколько хорошо работает чередование с Branchfree. - person Adisak; 09.10.2010
comment
@Adisak: все арифметические операторы расширяют до типа int любые операнды, которые меньше, чем int. Размер результата можно принудительно уменьшить с помощью приведения типов, но он снова увеличится при выполнении следующей арифметической операции. - person supercat; 09.10.2010
comment
@supercat: Да, мне нравится инструкция ARM IT (если-то), в которой есть быстрый предикат, позволяющий пропустить пару следующих инструкций. По-прежнему стоит знать, что на всех суперскалярных конвейерных архитектурах ARM версия с чередованием без ветвей будет в 3-4 раза быстрее, чем версия, использующая ИТ-инструкции. Это до использования SWAR / SIMD. Используя 32-битный SWAR без ветвей, он будет в 12–16 раз быстрее в самом сердце развернутого цикла. Конечно, при развертывании цикла вам нужны пролог / эпилоги цикла, поэтому может быть целесообразно, чтобы короткие строки проходили через IT-путь, а длинные строки проходили через мой. - person Adisak; 09.10.2010
comment
Хорошо, на самом деле тестировал код суперкадра в компиляторе (лучший способ проверить материал). Оно работает. Я думал, что переход на целое число - это новая функция в C #, но, очевидно, это всегда было и в C / C ++. Приятно узнавать что-то новое каждый день. Я всегда явно продвигал (путем приведения), когда мне нужно, чтобы операция была большего размера. - person Adisak; 09.10.2010
comment
@supercat: О, и IT-инструкция для ARM - это редкое исключение, когда все хорошие компиляторы фактически обычно генерируют инструкцию предиката. Хотя, похоже, они не так часто делают это на X86 :-( - person Adisak; 09.10.2010
comment
@Adisak: Я использую ARM-машины, которые позволяют условно выполнять или пропускать любую функцию в зависимости от состояния флагов ЦП, и каждой инструкции может быть разрешено либо изменять флаги ЦП, либо нет. Нет необходимости в явной инструкции If / Then. Я не знаю, в какой степени компиляторы могут использовать условное выполнение, но если в предложении if / else будет три или меньше инструкций в каждом условии, лучше условно выполнить все (до) шести инструкций, чем выполнять переход. Кстати, я опубликовал решение в стиле SIMD; Что вы думаете об этом? - person supercat; 10.10.2010
comment
@supercat: инструкция if-then (IT) - это версия предикатов для ARM. Он определяет, выполняются ли следующие N инструкций на основе флагов ЦП без ветвления. Я думаю, мы говорим об одном и том же. IT-инструкция является частью Thumb-2 IS. en.wikipedia.org/wiki/ARM_architecture IT (если-то) инструкция, который позволяет выполнять до четырех последовательных инструкций на основе проверенных условий. - person Adisak; 10.10.2010
comment
Ах, я думал в терминах 32-битных кодов операций ARM, а не Thumb. 32-битные коды операций включают в себя 4-битное поле условия, которое позволяет условно выполнять любую инструкцию, а также бит S, который указывает, должна ли команда влиять на состояние. Я не особо смотрел на набор инструкций Thumb, так как немногочисленное программирование на ARM, которое я сделал, было чрезвычайно критичным по скорости. - person supercat; 11.10.2010
comment
@Supercat: Новые чипы ARM обрабатывают инструкции Thumb на полной скорости ... иногда они даже работают быстрее, потому что занимают меньше места в кеше инструкций, поэтому вы получаете больше обращений к кешу. Единственное предостережение: цели ветвления должны быть выровнены по 32 бита, иначе вы можете потерпеть неудачу при декодировании. - person Adisak; 09.11.2010
comment
@Adisak: Я ожидал, что выполнение одной инструкции Thumb займет такое же время, как и одной инструкции ARM, но если для выполнения работы одной инструкции ARM потребуются две или три инструкции Thumb, эти инструкции Thumb будут занимать 2-3 раза пока. Одна вещь, которую я подумал, будет симпатичной, - это набор инструкций, в котором используется 32-битный код операции, но используется 1/4 пространства кода операции, чтобы два 15-битных кода операции могли быть сохранены в слове, не требуя отдельного режима. Я не знаю ни одного процессора, который бы это делал, но я думаю, что это возможно. - person supercat; 13.11.2010
comment
Примечание для будущих читателей - на самом деле ветвления нет, несмотря на то, что здесь предлагают многие комментарии; операция сравнения - это отдельная инструкция, которая просто устанавливает флаги компилятора, но она не требует и никуда не перескакивает, а вместо этого имеет очень похожий эффект на add / sub / .... - person RReverser; 17.06.2016
comment
Мой код 8088 для toupper (несколько десятилетий назад) состоял из 6 инструкций и по существу эквивалентен приведенному выше коду C: (mov cl, al; sub cl, 'a'; cmp cl, 26; sbb cl, cl; and cl, - 32; добавить al, cl) - person Aaron West; 14.03.2019

ПРИМЕЧАНИЕ. Заголовок вопроса был отредактирован - исходный заголовок был посвящен оптимизации «Пожалуйста, критикуйте - оптимальная функция для преобразования строковых регистров в C», что объясняет, почему мой ответ касается только оптимизации, а не общего характера. "улучшение" функций.

Если вы действительно ищете самый быстрый способ сделать это, то в долгосрочной перспективе вам подойдет версия без веток, поскольку она может использовать SIMD. Кроме того, он позволяет избежать таблиц (которые могут быть слишком большими во встроенной системе, если память действительно ограничена).

Вот простой пример без ветвей без SIMD, и ToLower - тривиальное изменение.

char BranchFree_AsciiToUpper(char inchar) 
{ 
        // Branch-Free / No-Lookup 
        // toupper() for ASCII-only 
        const int ConvertVal = 'A' - 'a'; 
        // Bits to Shift Arithmetic to Right : 9 == (char-bits + 1) 
        const int AsrBits = 9; 

        int c=(int)inchar; 
        //if( (('a'-1)<c) && (c<('z'+1)) ) { c += 'A'-'a'; } 
        int LowerBound = ('a'-1) - c; 
        int UpperBound = c - ('z' + 1); 
        int BranchFreeMask = (LowerBound & UpperBound)>>AsrBits;
        c = c + (BranchFreeMask & ConvertVal); 
        return((char)c); 
}

Моя функция расширена для ясности и использует не жестко заданные константы. Вы можете сделать то же самое в одной строке с жестко запрограммированными значениями, но мне нравится читаемый код; однако вот «сжатая» версия моего алгоритма. Это не намного быстрее, так как оно ТОЧНО то же самое "сжимает" до одной строки.

c+=(((96-(int)c)&((int)c-123))>>9)&(-32);

Здесь вы можете сделать ряд оптимизаций, чтобы сделать его еще быстрее. Вы можете жестко закодировать более оптимальные числа для ASCII, потому что в примере не предполагается, что какое-либо отображение кодировки, кроме a-z, и A-Z являются смежными диапазонами. Например, с ASCII, если у вас нет переключателя, вы можете фактически изменить AsrBits на 4 (9-5), поскольку ConvertVal будет +/- 32 в зависимости от операции toupper или tolower.

Если у вас есть рабочие версии без ветвей, вы можете использовать методы SIMD или SWAR (SIMD в регистре) для преобразования 4–16 байтов за раз (или даже, возможно, больше в зависимости от того, насколько широки ваши регистры, и если вы развернете, чтобы скрыть задержку). Это будет намного быстрее, чем любой метод поиска, который в значительной степени ограничен однобайтовым преобразованием, если только у вас нет очень больших таблиц, которые растут экспоненциально на байт, обрабатываемый одновременно.

Кроме того, вы можете сгенерировать предикат без ветвей, не используя повышающее преобразование int, но тогда вам нужно будет выполнить еще пару операций (с повышающим преобразованием это всего лишь одно вычитание на диапазон). Возможно, вам потребуется выполнить расширенные операции для SWAR, но в большинстве реализаций SIMD есть операция сравнения, которая бесплатно генерирует для вас маску.

Операции SWAR / SIMD также могут выиграть от меньшего количества операций чтения / записи в память, а выполняемые записи могут быть выровнены. Это намного быстрее на процессорах, которые имеют штраф за попадание при загрузке (например, на процессор ячеек PS3). Совместите это с простой предварительной выборкой в ​​развернутой версии, и вы почти полностью сможете избежать остановки памяти.

Я знаю, что в моем примере много кода, но есть ZERO веток (неявных или явных), и в результате нет неверных предсказаний ветвлений. Если вы работаете на платформе со значительными штрафами за неправильное прогнозирование ветвления (что верно для многих конвейерных встроенных процессоров), то даже без SIMD ваша оптимизированная сборка выпуска приведенного выше кода должна работать быстрее, чем что-то, что кажется гораздо менее сложным, но создает неявные ветви.

Даже без SIMD / SWAR интеллектуальный компилятор может развернуть и чередовать вышеуказанную реализацию, чтобы скрыть задержки и получить очень быструю версию - особенно на современных суперскалярных процессорах, которые могут выдавать более одной независимой инструкции за цикл. Обычно это невозможно ни с одной из версий ветвления.

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

// Unrolled inner loop where 'char *c' is the string we're converting
char c0=c[0],c1=c[1],c2=c[2],c3=c[3];  // Grouped-Loads
c[0]=BranchFree_AsciiToUpper(c0);
c[1]=BranchFree_AsciiToUpper(c1);
c[2]=BranchFree_AsciiToUpper(c2);
c[3]=BranchFree_AsciiToUpper(c3);
c+=4;

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

mr               r31,r3
mr               r13,r13
lbz              r11,0(r31)
lbz              r10,1(r31)
extsb            r11,r11
lbz              r9,2(r31)
extsb            r10,r10
lbz              r8,3(r31)
subfic           r7,r11,96
addi             r6,r11,-123
srawi            r5,r7,9
srawi            r4,r6,9
subfic           r3,r10,96
addi             r7,r10,-123
extsb            r9,r9
srawi            r6,r3,9
srawi            r3,r7,9
subfic           r7,r9,96
addi             r30,r9,-123
extsb            r8,r8
srawi            r7,r7,9
srawi            r30,r30,9
subfic           r29,r8,96
addi             r28,r8,-123
srawi            r29,r29,9
srawi            r28,r28,9
and              r5,r5,r4
and              r3,r6,r3
and              r7,r7,r30
and              r30,r29,r28
clrrwi           r4,r5,5
clrrwi           r6,r7,5
clrrwi           r5,r3,5
clrrwi           r7,r30,5
add              r4,r4,r11
add              r3,r5,r10
add              r11,r6,r9
stb              r4,0(r31)
add              r10,r7,r8
stb              r3,1(r31)
stb              r11,2(r31)
stb              r10,3(r31)

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

Итак, причины, по которым это должен быть самый быстрый способ:

  1. Отсутствие штрафов за неверное предсказание ветвления
  2. Возможность SIMD-алгоритма для 4-16 (и более) байтов за раз
  3. Компилятор (или программист) может разворачивать и чередовать, чтобы устранить задержки и воспользоваться преимуществами суперскалярных (многозадачных) процессоров.
  4. Нет задержек памяти (например, поиск по таблице)
person Adisak    schedule 07.10.2010
comment
Можете ли вы описать свой подход к написанию алгоритма без ветвей? Можете ли вы создать алгоритм без ветвлений, применив некоторые эвристики и преобразования к наивному алгоритму, требующему ветвления? - person Jeremy W. Sherman; 04.11.2010
comment
@ Джереми: Ну, это простой случай выбора между двумя значениями. Это просто поиск критериев для создания двоичной маски для условия, которое выбирает одно из двух значений, а затем использование маски для выбора значения. Это довольно простой подход. В большинстве случаев один и тот же алгоритм или алгоритм с очень простыми модификациями можно применять параллельно. Я не показывал этого, но показываемый мной пример развертывания также довольно тривиален. - person Adisak; 09.11.2010
comment
довольно впечатляюще, хотелось бы увидеть реализацию SIMD. Я никогда не думал, что можно купить машину без веток. - person Rafael; 06.10.2016

Я не решился ответить на этот вопрос, потому что прошло более 20 лет с тех пор, как я работал с небольшими устройствами. Однако я думаю, что правила почти такие же (с одним возможным дополнением):

  1. Минимизируйте доступ к памяти
  2. Минимизируйте циклы ЦП
  3. Минимизировать размер кода

Когда я разрабатывал низкоуровневый код, правило №1 затмило все остальные. На плате не было кэш-памяти, а память была невероятно медленной по сравнению с процессором; это причина того, что класс хранения «регистр» существует в C. Сегодня ситуация несколько изменилась, но это все еще одна из двух основных проблем. Как я прокомментировал к одному сообщению, таблица поиска - хорошая идея, но знайте, что это означает дополнительный доступ к памяти для каждого теста. Как только он попадает в кеш, это может не быть проблемой, но вы будете платить за несколько попаданий в кеш каждый раз, когда вводите функцию (если вы не вызываете ее так часто, что таблица поиска может оставаться в кеше) .

Правило номер 2 похоже на «да, конечно, вы хотите это сделать, почему это не правило №1?» но на самом деле рассуждения идут глубже. Фактически, в некотором смысле это повторение правила №1, поскольку каждая инструкция должна быть извлечена из памяти, прежде чем она может быть выполнена. Здесь есть тонкий компромисс: для процессора, работающего только с целыми числами, очевидным преимуществом является использование таблицы поиска для вычисления тригонометрических функций; на микросхеме со встроенной плавающей запятой, может, и нет.

Я не уверен, что правило № 3 все еще применяется. По моему опыту, всегда были попытки сократить код, уместить пресловутые 20 фунтов в 10-фунтовый мешок. Но похоже, что сегодня самый маленький мешок весит 50 фунтов. Однако даже с 50-фунтовым мешком (или многомегабайтным ПЗУ) для хранения вашего кода / данных вам все равно нужно вытащить его в кеш (если он у вас есть).

Новое правило №1: держите конвейер заполненным

Современные процессоры имеют глубокие конвейеры команд (если вы не знакомы с этим термином, см. Эту статью: http://arstechnica.com/old/content/2004/09/pipelining-1.ars/1). Общее практическое правило с глубокими конвейерами состоит в том, что ветвление - тест «если» - дорого обходится, потому что это означает, что конвейер, возможно, придется очистить для загрузки в новый код. Таким образом, вы пишете свой код для ветвления в маловероятном случае (см. Сообщение Adisak для возможно оправданной реализации без ветвления; +1, если можно).

Кто-то с более свежим опытом, чем я, вероятно, прокомментирует и скажет, что «современные процессоры загружают конвейер с обеими ветвями, поэтому нет штрафных затрат». Это все хорошо, но это приводит к общему правилу:

Правило 0: оптимизация зависит от вашей архитектуры и рабочей нагрузки

Микропроцессор в моей посудомоечной машине, вероятно, не имеет конвейера и, возможно, не имеет кеша. Конечно, он, вероятно, также не будет много обрабатывать текст. Или, может быть, есть и то, и другое; кажется, что на рынке всего несколько основных встраиваемых процессоров, так что, возможно, на этой плате стоит Pentium, а не производная от 8051. Даже в этом случае существует широкий спектр встроенных процессоров на базе Pentium (http://en.wikipedia.org/wiki/List_of_Intel_Pentium_microprocessors#Embedded_processors). То, что лучше для одного, может оказаться не лучшим для другого.

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

Однако есть еще кое-что: я прокомментировал «Только ASCII, а?» на ОП; другой комментатор был более явным: если вы обрабатываете текст в 2010 году, вы, вероятно, не обрабатываете ASCII. По крайней мере, вы будете иметь дело с ISO-8859-1 или аналогичным 8-битным набором символов. И в этом случае, возможно, решение no-branch или smart-branch (обращая внимание на конвейер) все равно будет быстрее, чем таблица поиска (да, это мое предположение). Но если вы имеете дело с Unicode BMP (16 бит), вам в значительной степени придется использовать эту таблицу, независимо от ее стоимости с точки зрения памяти, потому что нет простых правил, чтобы определить, что нижний, а какой верхний регистр. И если вы имеете дело с высшими уровнями Unicode ... ну, может быть, использование заглавных букв в "старом курсиве" не так важно (особенно потому, что в нем нет верхнего и нижнего регистра).

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

И наконец: очистить код FTW

Этот пост начался, когда я написал комментарий к OP о том, что его / ее использование макросов было плохой идеей (и не смог ввести его, потому что SO перешел в режим обслуживания). Питер Торок (извините, я не поддерживаю Unicode или даже ISO-8859-1) назвал одну причину, но есть и другую: это черные ящики.

OP выглядит красиво и чисто: короткий код, интенсивное использование побитовых и тернарных операторов, его легко понять, если вы понимаете язык. Но было бы намного легче понять реальную работу, если бы вы увидели A_Z в развернутом виде. Это могло заставить вас задуматься о том, сколько ветвлений вы выполняли, особенно в методе ToggleCase. А затем вы, возможно, подумали о том, как можно переупорядочить эти ветки, чтобы минимизировать количество реальных тестов, которые вы выполняете. И, возможно, подумал о поддержании трубопровода.

person Anon    schedule 09.10.2010

Хорошо, поехали. Запись на этой вкладке ... прокрутка кода на другой вкладке :-)

заголовок

  1. #define a_z(c) (c >= 'a' && c <= 'z')

    • the name of the function like macro should be in ALL CAPS (maybe IS_LOWERCASE) to alert users it's a macro
    • c в расширении должен быть внутри скобок, чтобы предотвратить странные побочные эффекты
    • личный выбор: мне нравится переупорядочивать условия, чтобы они больше походили на английское 'a' ‹= c‹ = 'z' как (('a' <= (c)) && ((c) <= 'z'))
  2. Я бы сделал так, чтобы функции void ToggleCase(char* c) возвращали char* (то же, что было отправлено), чтобы можно было использовать их последовательно: printf("%s\n", UpperCase(LowerCase("FooBar")));

исходный код

  1. Тернарный оператор не делает ваш код более быстрым или легким для чтения. Я бы написал простой if

Вот и все.

Ой! Еще одна вещь: ваш код предполагает ASCII (вы сами так сказали), но не документирует это. Я бы добавил об этом примечание в заголовочный файл.

person pmg    schedule 07.10.2010
comment
Запись (('a' <= (c)) && ((c) <= 'z')) показывает, что никто не знает об использовании беззнаковой арифметики. ((unsigned)(c)-'a'<='z'-'a') - это канонический способ записать это, и он позволяет избежать многократной оценки аргумента. Я ненавижу операторы приведения, поэтому обычно пишу их как ((c)-'a'<26U), если знаю, что (c)-'a' не может переполниться, иначе ((c)-65U<26). - person R.. GitHub STOP HELPING ICE; 07.10.2010
comment
+1 за ненависть к кастам ... и спасибо за урок про беззнаковый трюк - person pmg; 07.10.2010
comment
Тернарный оператор не ускоряет ваш код - это правда. Это требуется для чего-то вроде инициализации константы, но на самом деле может замедлить ваш код, если вы используете его для не-внутренних типов. Это потому, что?: Может вызывать повышение типа, создание копирования и создание присваивания, а также создание временных объектов для оценки выражения для не внутренних типов. - person Adisak; 08.10.2010

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

ANSI C включает необходимые функции в стандартную библиотеку, и, по-видимому, они были сильно оптимизированы для вашей архитектуры поставщиком компилятора.

Стандартный заголовок ctype.h включает функции tolower () и toupper ().

person Steve Bohrer    schedule 04.11.2010

Первым делом я бы сказал переименовать a_z и A_Z во что-то вроде is_ASCII_Lowercase и is_ASCII_Uppercase. Это не так C-ish, но это легче понять.

Кроме того, использование ^= и ?: работает, но, опять же, я считаю его менее читаемым, чем простой if-оператор.

person FrustratedWithFormsDesigner    schedule 07.10.2010

Возможно, я провел слишком много времени с C ++ и недостаточно с C, но я не большой поклонник макросов, у которых есть параметры ... как отмечает Питер Торок, они могут привести к некоторым проблемам. Ваше определение CASE_FLAG в порядке (оно не принимает никаких параметров), но вместо этого я бы заменил макросы a_z и A_Z функциями.

person andand    schedule 07.10.2010
comment
У его макросов действительно есть проблемы в том, что они повторяют свои аргументы более одного раза. В его коде это нормально, но в целом это плохо (за исключением нескольких расширений компилятора, которые иногда можно использовать для исправления ситуации). С хорошим компилятором встроенные функции подойдут для этого, но с плохим компилятором макросы сделают код лучше (хотя исходный код далек от оптимального). - person nategoose; 07.10.2010
comment
Вы можете создавать макросы из моих версий, которые стараются не оценивать свои аргументы более одного раза. Смотрите мой ответ. - person R.. GitHub STOP HELPING ICE; 07.10.2010

как насчет (почти работает):

char slot[] = { 0, 31, 63, 63 };
*c = slot[*c/32] + *c%32;

Пара вещей, которые можно изменить:

*c += a_z(*c)*CASE_FLAG; // adds either zero or three two
// you could also replace multiplication with the shift (1<<5) trick

строки на самом деле массивы:

char upper[] = "ABC..ABC..."; // 
...
*c = upper[*c+offset];

or

char upper[] = "ABC.."; // 
...
*c = upper[*c%32];

or

*c = 'A' + *c%32;

или как там ...

person Anycorn    schedule 07.10.2010

Мой подход - «обрезать только при необходимости».

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

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

 enum CASE {UPPER, LOWER};

void ToggleCase(char* c, CASE newcase)
{
    if(newcase == UPPER)
       UpperCase(c);
    else if(newcase == LOWER)
       LowerCase(c);
    else 
       { ; } //null
}

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

void LowerCase(char* c)
{
  while (*c++)  //standard idiom for moving through a string.
  {
    *c = *c < 'Z' ? *c + 32 : *c;
  }
}


void UpperCase(char* c)
{
  while (*c++)
  {
    *c = *c > 'a' ? *c - 32 : *c;
  }
}

Теперь есть некоторые критические замечания в мой код.

Во-первых, ветвистый. Во-вторых, предполагается, что ввод [a-zA-Z] +. В-третьих, это только ASCII (как насчет EBDIC?). В-четвертых, предполагается нулевое завершение (некоторые строки имеют некоторые символы в начале строки - я думаю, Паскаль). В-пятых, не на 100% наивно очевидно, что код в верхнем / нижнем регистрах. Также обратите внимание, что ENUM - это плохо завуалированное целое число. Вы можете передать ToggleCase("some string", 1024), и он будет компилироваться.

Это не значит, что мой код очень плохой. Он служит и будет служить - только при определенных условиях.

person Paul Nathan    schedule 07.10.2010

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

Это более эффективно? Каковы ваши требования к размеру кода? (Для сгенерированного исполняемого кода, а не для исходного кода C.) В современных настольных системах это редко является проблемой, и скорость имеет гораздо большее значение; но вы не предоставили нам никаких дополнительных сведений, помимо «приложений для встроенных систем», поэтому мы не сможем ответить на этот вопрос за вас. Однако здесь это не проблема, потому что код внутри макросов действительно такой маленький, но вы не можете предположить, что избегание вызовов функций всегда более эффективно!

Вы можете использовать встроенные функции, если вам разрешено. Они официально являются частью C с 1999 года, но гораздо дольше поддерживаются в нескольких компиляторах. Встроенные функции намного чище, чем макросы, но, опять же, в зависимости от ваших точных целевых требований, может быть трудно предсказать сгенерированный код из источника. Однако чаще люди застревают на устаревших (уже более десяти лет!) Компиляторах C, которые их не поддерживают.

Короче говоря, вы всегда должны знать свои точные требования, чтобы определить, что является оптимальным. Затем вам необходимо выполнить тест, чтобы проверить свои прогнозы производительности.

person Community    schedule 08.10.2010

Если кто-то пытается обработать несколько байтов одновременно, я думаю, что лучшим подходом было бы заставить все значения быть 0..127, добавить 5 или 37 (что сделало бы 'z' к 'Z' равным 127), обратите внимание, что значение, а затем добавьте 26, обратите внимание на это значение, а затем немного измените. Что-то вроде:

unsigned long long orig,t1,t2,result;

t1 = (orig & 0x7F7F7F7F7F7F7F7F) + 0x0505050505050505;
t2 = t1 + 0x1A1A1A1A1A1A1A1A;
result = orig ^ ((~(orig | t1) & t2 & 0x8080808080808080) >> 2);

Хм ... Думаю, это неплохо работает, даже если адаптировано для 32-битной машины. Если в четыре регистра предварительно загружены правильные константы, ARM с оптимальным кодом, вероятно, сможет выполнять операции с семью инструкциями, занимая семь циклов; Я сомневаюсь, что компилятор найдет оптимизацию (или выяснит, что сохранение констант в регистрах было бы полезно - если константы не хранятся в регистрах, обработка байтов по отдельности будет быстрее).

person supercat    schedule 09.10.2010
comment
Вы можете замаскировать старший бит и использовать его как сквозной выбор, и тогда он будет работать для всех значений 0-255. Но если бы вы знали свой ASCII, было 0-127, вы могли бы пропустить лишнюю работу :-) - person Adisak; 10.10.2010