Комментарии 82
Ну наверное разработчики glibc наворотили это не от доброты душевной, а чтобы соответствовать стандарту, плюс быть совместимыми со старыми приложениями, которые вместо EOF используют -1 и при этом еще и поддерживать локали. И чтобы это все работало на процессорах с разным endianess.
Поэтому, я не совсем понимаю возмущение автора.
В glibc недостает assert-а, который-бы контролировал допустимость аргумента.
У меня даже стойкое дежавю, что лет 10 назад я то-ли субиметл такой патч, то-ли добавлял assert в заголовки на сборочной ферме.
В glibc недостает assert-а, который-бы контролировал допустимость аргумента.и который:
а) успешно выпиливается в релизной конфигурации
б) если не выпиливается, то кладёт на лопатки branch prediction
Так вы считаете что от assert-ов следует вообще отказаться по обозначенным "причинам"?
Заменит непонятное сообщение об ошибке на понятное, чтобы больше не пришлось гадать как это вообще возможно и лезть в потроха glibc.
Да, только в отладочной сборке.
Бесспорно, расковырять сорцы glibc полезно, особенно, если разобраться, зачем они сделали именно так, но исследование проблемы методом чтения документации тоже не заняло много времени.
Я тоже не настоящий разработчик на Си, и документацию я читать тоже умею. Вот только если код чужой — то может оказаться далеко не сразу очевидно, что нарушено именно это условие. Корректное сообщение об ошибке хотя бы в отладочной сборке надёжнее.
Как только прочёл предложение автора "Переменные last и next имеют тип uint32_t", сразу стало понятно, что проблема — в диапазоне чисел, осталось только пойти на https://en.cppreference.com/w/c/string/byte/isalnum за подтверждением. Минус бы автору за то, что специально до конца не раскрывал значение переменной next.
А ещё статическому анализатору поможет, который распознает assert и радостно сообщит «value out of range». (Но ни cppcheck, ни scan-build из clang-tools-7 в Debian 9 assert не помог увидеть ошибку.)
Хм, но почему простой assert кладёт на лопатки branch prediction? Вроде же наоборот, он должен без проблем им предсказываться...
Но и прироста производительности от него не будет: с точки зрения процессора, это не assert, а обычное сравнения (лишнее сравнение), какой-то переход и 2 ветки кода за ним. Через несколько итераций он начнет угадывать на какую ветку будет выполнен переход до сравнения.
Тесты в инете показывают что реализация glibc быстрее: https://paste.sr.ht/%7Eminus/18b44cfe58789bc1fb69494130e859a1189d1772
А вы можете как-то прокомментировать эти тесты? Я не настоящий сварщик и не пишу на Си, но если функция содержит две строки кода и выполняет только базовые операции над целыми числами, а работает на целый порядок медленнее — закрадывается подозрение, что сам тест написан неправильно.
С тестами всё нормально. Все упомянутые "навороты" в glibc, в том числе, для скорости.
Надо ассемблерный код обоих вариантов смотреть. Ощущение, будто в случае с glibc
функция заинлайнилась, а в случае с musl
— нет, и такое сравнение нельзя назвать корректным.
Реализация glibc избавлена от сравнений и условных переходов, в том числе изначально рассчитана на инлайнинг. Её примерно всегда дешевле заинлайнить чем вызвать, как по объему кода, так и по скорости.
В целом довольно наивно/глупо писать статью со словами "галиматья", "дурацкая херня" и т.п. (это я про автора, а переводчику респект) не понимая и 1/10 причин "зачем и почему" так сделано. А голосование довольно хорошо показывает "среднюю по больнице" квалификацию голосующих.
А голосование довольно хорошо показывает «среднюю по больнице» квалификацию голосующих.Судя по статьям про стандарт C++ можно и результаты голосования не открывать, так как там дофига комментариев типа «в моём Rust/Go/C#/Java/JavaScript/PHP всё понятно, такой фигни нет, дурачки из комитета/писатели компиляторов только и делают, что UB вводят».
Ну, собственно, потому в glibc и используются макросы вместо функций, что макросы гарантированно инлайнятся...
Подозреваю, что glibc-шный метод, со всей его работой с локалями сводится к массиву из 256 элементов, для которых заранее рассчитано, являются ли они «alphanumeric».
Что-то вроде
static bool g_isAlphanumeric[256] = initAlphanumericOnce();
bool isalnum(unsigned int c) { return g_isAlphanumeric[c]; }
Массив быстро попадает в кеш процессора, лишние разыменования указателей нивелируются способностью процессора выполнять несколько микроинструкций за такт: никто не мешает процессору тянуть данные из этой таблицы заранее, и никто не мешает в процессе доступа к памяти посчитать что-нибудь заранее на АЛУ. Пока какой-то if() из кода по типу интуитивной реализации isalnum() не обдурит предсказатель переходов, и весь конвейер не будет сброшен и перезапущен.
Я конечно тоже настоящий программист на C, но может лучше тогда так и было писать( с учётом локалей, если они могут повлиять на её результат )? Чтобы функция простой проверки не размазывалась на десяток макросов. Спасибо хоть на нестандартные расширения компилятора не полагается
256 байт в L1, там 384 байта в L1, здесь 1024 байта, и L1 кончился.Речь идет о нагруженных циклах с кучей итераций.
Речь идет о нагруженных циклах с кучей итерацийНагруженный цикл не может быть в 1000 инструкций длиной? С кучей алгоритмов, которым всем надо либо память, либо арифметику?
Обсуждался случай с обработкой текста, а не попытки засунуть 16-битное умножение в предварительно посчитанную таблицу. Спасибо, что не сложение.
Так вот, арифметика в обсуждаемом случае плоха тем, что она как минимум нагружает ALU, который можно освободить для других задач: например, для спекулятивного выполнения инкремента счетчика на единицу.
Посмотрите, сколько стоит cache miss.Не думал, что так можно, но раз можно, то почитайте про «instructions per cycle» в контексте профайлинга.
И чтобы не быть голословным, давайте я приведу результаты бенчмарка: https://quick-bench.com/q/ET57hypxMHbRNEfWYNoR_oNT88A
- is_alphanumeric таблица быстрее в 3 раза
- is_alpha таблица быстрее на 40%
- to_upper таблица переводит в верхний регистр в 2.3 раза быстрее
- проверка байта на четность (куда уж проще) практически одинаковая, таблица на 10% быстрее
- нагрузим L1 посильнее: сборная солянка из 4 таблиц в одном цикле is_alnum+is_alpha+is_even+to_upper быстрее того же с расчетом на лету на 80%
В тестах и некоторых примерах, конечно, всё хорошо.
Я не пытаюсь доказать, что арифметика всегда лучше. Я только хочу отметить, что не надо всё перекладывать на таблицы. Пусть и небольшие в каждой функции, в сумме они могут превысить предел L1.
так он прямо об этом написал:
Теперь становится очевидно, как исправить проблему. Мой косяк. Оказывается, я не могу скормить в isalnum произвольный символ UCS-32 для проверки на его вхождение в диапазоны 0x30-0x39, 0x41-0x5A и 0x61-0x7A
а вообще от этих локалей одни проблемы. Хорошо бы выкинуть их к чертовой бабушке и везде пользоваться UTF-8.
Не поможет, локаль — это не только кодировка, один и тот же Юникод в разных локалях может работать по-разному. И если на результат isalnum
культура не влияет — то вот tolower
от локали даже при использовании Юникода зависит.
Наиболее известный пример — турецкая локаль, с их знаменитыми символами İ
и ı
.
Но такое ощущение, что он так и не понял, ЗАЧЕМ так сделано, и до конца продолжает нахваливать библиотеку с наивной (и не вполне корректной) реализацией isalpha() как образец для подражания. В общем, ИМХО, rant высосан из пальца.
С другой стороны, передавать char32_t или его аналог в функцию, которая принципиально работает только с char | EOF
— значит вообще не понимать что делаешь.
Ради скорости. Выше уже обсудили это.
А при чём тут кодировка? Функция не упадёт пока в неё передаётся char
, какую бы кодировку этот char
не использовал.
int isalnum(int c);
Один и тот же символ в разных кодировках выглядит очень по разному
echo -n я|iconv -t cp866|hexdump -C
00000000 ef |.|
echo -n я|iconv -t cp1251|hexdump -C
00000000 ff |.|
echo -n я|iconv -t utf8|hexdump -C
00000000 d1 8f |..|
cho -n я|iconv -t utf32|hexdump -C
00000000 ff fe 00 00 4f 04 00 00 |....O...|
Если не хотим, чтобы программа падала — нужно уметь проверять int на валидность, причем с учетом кодировки.
И что? Это же Си, тут много где int. К слову, для первых двух ваших вариантов isalnum работает не падая, а для последних двух она просто неприменима.
Это же Си, тут много где int.
Скорее совместимость со стандартами, где положено чтобы на вход подавалось unsigned char + EOF (-1).
Получается что есть 2 функции с одинаковыми параметрами (isalnum и isalnum_l), но одна с wide chars не работает (и даже на некоторых может упасть), а вторая — работает, но требует дополнительных движений. Жаль, автор умолчал, на каком именно символе оно падает.
#include <stdio.h>
#include <locale.h>
int main()
{
int c = 0x410; // русская А
setlocale(LC_ALL,"");
locale_t l = duplocale(LC_GLOBAL_LOCALE);
printf("isalnum(%d)=%d\n", c, isalnum_l(c, l));
return 0;
}
LANG=ru_RU.UTF-8 ./a.out
isalnum(1040)=8
Логично, вернула не 0.
LANG=en_US.UTF-8 ./a.out
isalnum(1040)=8
LANG=C ./a.out
isalnum(1040)=0
Что-то я не вижу в стандарте требования чтобы isalnum_l работала со значением 0x410:
The c argument is an int, the value of which the application shall ensure is representable as an unsigned char or equal to the value of the macro EOF. If the argument has any other value, the behavior is undefined.
Нет. Это парадигма С — Если что-то не по стандарту, то можно хоть диск форматировать. За счет этого отсутсвуют всякие проверки и код работает на порядок быстрее. Но программисту нельзя допускать ошибки, да.
Особенно раньше, когда к пользователям не отсносились вроде "пусть купит еще плашку памяти и процессор помощнее", это было важно.
А для тех кто на бронетанке можно пожалуйста поподробнее про эту историю?
Насчет "потенциально быстрее" Линус в паре комментариев ниже подробно пояснил, почему это полная ерунда.
and memmove() simply just checks whether the destination address is above the source address and decides to copy backwards if so
Знаете ли я тоже имею некоторое представление о сложности этого «simply just checks», т.к. задавался конкретно этим вопросом. Я не сишник и сишный стандарт как отче наш на знаю, но вот в мире с++ это нифига не простая проверка и думаю си здесь не отличается от с++. Если интересно за подробностями сюда
Так что, то что пишет Линус о том, что это чушь это он мегко говоря не прав в общем случае. Конечно, на тех платформах с которыми он привык работать наверное это действительно просто, но не на всех.
Т.е. то что он предлагал — вернуть назад простую наивную реализацию, которая также еще и оверлапинг проверяла, нормальное предложение, но оно по очевидным для меня причинам (не работает в общем случае) было отвергнуто. То что он назвал граничными случаями наверное(код не смотрел) и есть учет разных экзотических платформ — реализация общего случая корректным образом.
Еще раз перевожу с Линксвого языка на человеческий — я вертел вашу корретность в общем случае, ваша реализция поламала код который нормально работал для пользователей моего продукта, так шо давай те вашу корректность засуним куда подальше и поможем нашим пользователям. Нормальный, здравый подход защищать свой продукт и продавливать решения которые позволяют избежать работы. Я бы тоже такую дичь говорил если бы был заинтересованной (в таком решении) стороной.
Нет, спасибо, не надо, пусть лучше будет впервую очередь коректно и во второую максимально быстро, а за дополнительными плюшками идите в memmov.
+100500
и до конца продолжает нахваливать библиотеку с наивной (и не вполне корректной) реализацией isalpha() как образец для подражания
Все правильно он делает. Код, даже системных библиотек, должен, внезапно, писаться для... людей! Не для компилятора, а для людей. Потому что пройдет еще какое-то время, и разгрести это говнище в glibc станет просто некому.
Даже если вместо вычислений используются таблицы, то и оформить таблицы нужно по человечески, а не пятью уровнями макросов с не очевидными ветками условной компиляции.
Хорошо бы выкинуть их к чертовой бабушке и везде пользоваться UTF-8.
Второй пункт прямо противоречит первому
Если вы посмотрите на эффективные алгоритмы умножения матриц, то там тоже будет плохочитаемый код. Увы, но красивый и быстрый код почти всегда антонимы. Важно, чтобы хотя бы интерфейсы были читаемы и просты.
Такой вот у нас в C isalnum. Понять это нельзя, это можно только запомнить.
Я просто обожаю иметь дело с локалями.
Иногда я открываю вот этот коммит, чтобы проникнуться величием сишных локалей.
А где тут история библиотек?
Наверное правильнее было бы перевести как "рассказ о ..." (в оригинале a tale of).
А вот почему в glibc так написано — непонятно, явно преследовали какую-то цель, которую можно посмотреть по коммитам, и для этого точно требовались все эти средства.
Как минимум, эта реализация, в отличие от musl, поддерживает локали (что предписано стандартом).
А столько макросов навёрнуто вероятно ради быстрой работы на всех поддерживаемых архитектурах.
В голосовалке не хватает опции "Кривой стандарт ISO C". Почему не бы дополнить описание функции isalnum тем, что на всех остальных значениях возвращать false?
Как ни крути, если в функцию, работающую только с однобайтными кодировками передавать двухбайтный символ, в программе будет баг.
Вопрос только в том, сделать ли его детерминированным и одинаковым для всех реализаций стандартной библиотеки (и, соответственно, замедлить работу даже для корректных значений) или разрешить реализациям не обрабатывать специальным образом некорректные значения (чем glibc довольно эффективно пользуется).
И сразу медленнее! Нашли как выкрутится сейчас — найдут и как выкрутиться потом.
ISO C требует, чтобы функции ctype работали
со значениями типа `unsigned char' и EOF; мы также поддерживаем отрицательные
значения `signed char' для совместимости со старыми некорректными программами
То есть, чуть-чуть, но они отступают от принципа «никаких компромисов». Добавили лишних 768 байт в таблицах, чтобы некорректный код не падал.
Автор не прав, потому что он не знает того, с чем он работает. Для работы с символами UCS-32 есть набор функций с префиксом "w" (wide), в частности iswalnum для его целей. Удивительно, что за четыре года никто не указал на это.
Для работы с символами UCS-32 есть набор функций с префиксом "w" (wide), в частности iswalnum для его целей
Поправьте меня, но это же неправда? Стандарт никак не диктует, как кодировать wide charcter.
C11 3.7.3 wide character
value representable by an object of type wchar_t, capable of representing any character in the current locale
6.10.8.2 Environment macros
__STDC_ISO_10646__ An integer constant of the form yyyymmL (for example, 199712L). If this symbol is defined, then every character in the Unicode required set, when stored in an object of type wchar_t, has the same value as the short identifier of that character. The Unicode required set consists of all the characters that are defined by ISO/IEC 10646, along with all amendments and technical corrigenda, as of the specified year and month. If some other encoding is used, the macro shall not be defined and the actual encoding used is implementation-defined.
J.3 Implementation-defined behavior
J.3.4 Characters
The encoding of any of wchar_t, char16_t, and char32_t where the corresponding standard encoding macro (__STDC_ISO_10646__, __STDC_UTF_16__, or __STDC_UTF_32__) is not defined (6.10.8.2).
То есть, если указанный макрос не определён, то в wchar_t может быть не UCS. А ещё там может быть меньше 32 бит.
P.S. C17 — это C11 с мелкими исправлениями, ничего нового там нет.
Всё верно, в актуальном MSVC из Visual Studio 2022, sizeof(wchar_t) == 2
Это какой-то некропостинг, и я не буду заново читать пост и въезжать в тему, но на сколько я бегло посмотрел - у автора там linux, а на linux там UCS-32. Стало быть, я ответил, имея ввиду то, с чем он реально имеет дело.
И чтобы там в стандарте не было написано, полагаю, что вы не станете спорить с тем, что для wide char'ов надо юзать iswalnum, а не isalnum?
У автора Си и Implementation-defined behavior (IDB): вот про это речь в статье.
С вероятностью 99% автору не нужно UCS: судя по описанию, он как раз хотел категоризовать символы из ASCII и полагал, что символы вне первой страницы Unicode (читай, вне ASCII) будут отмечены как не попадающие в категорию.
Если бы автор кода скормил бы в isalnum число из диапазона "значение которого должно помещаться в unsigned char, или же равняться значению макроса EOF", то никакой проблемы бы не было. А segfault случился из-за того, что в int залетело число, не попадающее в этот диапазон. А как оно могло туда попасть? Очень просто - до этого были операции с wide char'ами, в результате которых родился wchar_t, который под линуксом представляет собой UCS-32 - его-то и скормили в isalnum.
История двух стандартных библиотек Си