История двух стандартных библиотек Си

Автор оригинала: Drew DeVault
  • Перевод
Сегодня мне пришел баг-репорт от пользователя Debian, который скормил какую-то ерунду в утилиту scdoc и получил SIGSEGV. Исследование проблемы позволило мне провести отличное сравнение между musl libc и glibc. Для начала посмотрим на стектрейс:

==26267==ERROR: AddressSanitizer: SEGV on unknown address 0x7f9925764184
(pc 0x0000004c5d4d bp 0x000000000002 sp 0x7ffe7f8574d0 T0)
==26267==The signal is caused by a READ memory access.
    0 0x4c5d4d in parse_text /scdoc/src/main.c:223:61
    1 0x4c476c in parse_document /scdoc/src/main.c
    2 0x4c3544 in main /scdoc/src/main.c:763:2
    3 0x7f99252ab0b2 in __libc_start_main
/build/glibc-YYA7BZ/glibc-2.31/csu/../csu/libc-start.c:308:16
    4 0x41b3fd in _start (/scdoc/scdoc+0x41b3fd)

В исходниках на данной строчке написано вот что:

if (!isalnum(last) || ((p->flags & FORMAT_UNDERLINE) && !isalnum(next))) {

Подсказка: p — это корректный, ненулевой указатель. Переменные last и next имеют тип uint32_t. Сегфолт случается на втором вызове функции isalnum. И, самое важное: воспроизводится только при использовании glibc, но не musl libc. Если вам пришлось перечитать код несколько раз, вы не одиноки: тут попросту нечему вызывать сегфолт.

Поскольку было известно, что все дело в библиотеке glibc, я достал ее исходники и стал искать реализацию isalnum, готовясь столкнуться с какой-нибудь дурацкой херней. Но прежде чем я дойду до дурацкой херни, которой там, уж поверьте, навалом, давайте сначала быстренько поглядим на хороший вариант. Вот так функция isalnum реализована в musl libc:

int isalnum(int c)
{
	return isalpha(c) || isdigit(c);
}

int isalpha(int c)
{
	return ((unsigned)c|32)-'a' < 26;
}

int isdigit(int c)
{
	return (unsigned)c-'0' < 10;
}

Как и ожидалось, для любого значения c функция отработает без сегфолта, потому что с какого хрена вообще isalnum должна кидать сегфолт?

Ладно, теперь сравним это с реализацией glibc. Как только вы откроете заголовок, вас будет встречать типичная GNU'шная галиматья, но пропустим ее и попытаемся найти isalnum.

Первый результат такой:

enum
{
  _ISupper = _ISbit (0),        /* UPPERCASE.  */
  _ISlower = _ISbit (1),        /* lowercase.  */
  // ...
  _ISalnum = _ISbit (11)        /* Alphanumeric.  */
};

Похоже на деталь реализации, двигаемся дальше.

__exctype (isalnum);

Но что такое __exctype? Возвращаемся на несколько строчек вверх…

#define __exctype(name) extern int name (int) __THROW

Ладно, по всей видимости это только прототип. Непонятно правда, зачем тут нужен макрос. Ищем дальше…

#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...

Так, вот это уже похоже на что-то полезное. Что такое __isctype_f? Мотаем вверх…

#ifndef __cplusplus
# define __isctype(c, type) \
  ((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
#elif defined __USE_EXTERN_INLINES
# define __isctype_f(type) \
  __extern_inline int                                                         \
  is##type (int __c) __THROW                                                  \
  {                                                                           \
    return (*__ctype_b_loc ())[(int) (__c)] & (unsigned short int) _IS##type; \
  }
#endif

Ну начинается… Ладно, вместе как-нибудь разберемся. Видимо, __isctype_f — это инлайн-функция… стоп, это все находится внутри блока else препроцессорной инструкции #ifndef __cplusplus. Тупик. Где же isalnum, мать ее, на самом деле определена? Ищем дальше… Может вот оно?

#if !defined __NO_CTYPE
# ifdef __isctype_f
__isctype_f (alnum)
// ...
# elif defined __isctype
# define isalnum(c)     __isctype((c), _ISalnum) // <- вот тут

Эй, это же «деталь реализации», которую мы видели раньше. Помните?

enum
{
  _ISupper = _ISbit (0),        /* UPPERCASE.  */
  _ISlower = _ISbit (1),        /* lowercase.  */
  // ...
  _ISalnum = _ISbit (11)        /* Alphanumeric.  */
};

Попробуем по-быстрому расковырять этот макрос:

# include <bits/endian.h>
# if __BYTE_ORDER == __BIG_ENDIAN
#  define _ISbit(bit)   (1 << (bit))
# else /* __BYTE_ORDER == __LITTLE_ENDIAN */
#  define _ISbit(bit)   ((bit) < 8 ? ((1 << (bit)) << 8) : ((1 << (bit)) >> 8))
# endif

Ну что за херня? Ладно, двигаем дальше и считаем, что это просто магическая константа. Другой макрос называется __isctype, который похож на недавно виденный нами __isctype_f. Посмотрим еще разок на ветку #ifndef __cplusplus:

#ifndef __cplusplus
# define __isctype(c, type) \
  ((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
#elif defined __USE_EXTERN_INLINES
// ...
#endif

Эээ…

Ну, по крайней мере мы нашли разыменовывание указателя, которое могло бы объяснить сегфолт. А что такое __ctype_b_loc?

/* Эти объявления находятся в файле ctype-info.c.
   Они должны совпадать с объявлениями в файле localeinfo.h.

   В модели локалей, основывающейся на потоках, (см. `uselocale' в <locale.h>)
   мы не можем пользоваться глобальными переменными, как раньше.
   Вместо этого, ниже указаны функции-аксессоры, которые возвращают адрес
   каждой переменной, относящейся к текущему потоку, если потоков несколько.

   Они указывают на массивы из 384 элементов, поэтому для индексации можно
   использовать значение `unsigned char' [0,255]; а также EOF (-1); или любое
   значение `signed char' value [-128,-1).  ISO C требует, чтобы функции ctype работали
   со значениями типа `unsigned char' и EOF; мы также поддерживаем отрицательные
   значения `signed char' для совместимости со старыми некорректными программами.
   Массивы для конвертации регистра содержат значения типа `int`,
   а не `unsigned char`, потому что `tolower(EOF)' должно возвращать EOF, а это значение
   не помещается в диапазон `unsigned char`. Однако сейчас самое важное - то, что
   эти массивы также используются для многобайтовых кодировок.  */
extern const unsigned short int **__ctype_b_loc (void)
     __THROW __attribute__ ((__const__));
extern const __int32_t **__ctype_tolower_loc (void)
     __THROW __attribute__ ((__const__));
extern const __int32_t **__ctype_toupper_loc (void)
     __THROW __attribute__ ((__const__));

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

(gdb) print ((unsigned int **(*)(void))__ctype_b_loc)()[next]
Cannot access memory at address 0x11dfa68

Сегфолт найден. В комменте была про это строчка: «ISO C требует, чтобы функции ctype работали со значениями типа `unsigned char' и EOF». Если найти это в спецификации, увидим:

Во всех реализациях [функций, объявленных в ctype.h], аргументом является int, значение которого должно помещаться в unsigned char, или же равняться значению макроса EOF.

Теперь становится очевидно, как исправить проблему. Мой косяк. Оказывается, я не могу скормить в isalnum произвольный символ UCS-32 для проверки на его вхождение в диапазоны 0x30-0x39, 0x41-0x5A и 0x61-0x7A.

Но тут я возьму на себя смелость предположить: а может быть, функция isalnum вообще не должна кидать сегфолт, вне зависимости от того, что ей передадут? Может быть, даже если спецификация это разрешает, это не означает, что так надо делать? Может быть, ну просто в качестве безумной идеи, поведение этой функции не должно содержать пять макросов, проверять использование компилятора C++, зависеть от порядка байтов вашей архитектуры, таблицы поиска, данных о локали потока и разыменовывать два указателя?

Еще разок посмотрим на версию в musl в качестве быстрого напоминания:

int isalnum(int c)
{
	return isalpha(c) || isdigit(c);
}

int isalpha(int c)
{
	return ((unsigned)c|32)-'a' < 26;
}

int isdigit(int c)
{
	return (unsigned)c-'0' < 10;
}

Вот такие пироги.

Примечание переводчика: спасибо MaxGraey за ссылку на оригинал.

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

Ваше мнение

  • 41,3%Автор прав: в glibc перемудрили169
  • 28,4%Автор неправ: стандарт есть стандарт116
  • 30,3%Мне все равно — как хорошо, что я не пишу на си124

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 75

    +23

    Ну наверное разработчики glibc наворотили это не от доброты душевной, а чтобы соответствовать стандарту, плюс быть совместимыми со старыми приложениями, которые вместо EOF используют -1 и при этом еще и поддерживать локали. И чтобы это все работало на процессорах с разным endianess.
    Поэтому, я не совсем понимаю возмущение автора.

      +3

      В glibc недостает assert-а, который-бы контролировал допустимость аргумента.


      У меня даже стойкое дежавю, что лет 10 назад я то-ли субиметл такой патч, то-ли добавлял assert в заголовки на сборочной ферме.

        –1
        В glibc недостает assert-а, который-бы контролировал допустимость аргумента.
        и который:
        а) успешно выпиливается в релизной конфигурации
        б) если не выпиливается, то кладёт на лопатки branch prediction
          +6

          Так вы считаете что от assert-ов следует вообще отказаться по обозначенным "причинам"?

            +2
            Я считаю, что слово «причины» в данном случае не следует брать в кавычки, что в релизном билде ассерты не помогут, и что если кому-то нужна версия isalnum с валидацией параметров, то её всегда можно написать и назвать isalnum_safe.

              +6
              ну и к слову, а что решит assert? Заменит «segmentation fault» внутри isalnum() на «assertion failure», но только в отладочной сборке?
                +14

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


                Да, только в отладочной сборке.

                  +12
                  Признаюсь: я не настоящий разработчик на С, я маску на стройке нашел. Как плюсовик, когда стандартная библиотека начинает меня удивлять, я иду на cppreference.com и в третьем предложении документации к функции читаю: «The behavior is undefined if the value of ch is not representable as unsigned char and is not equal to EOF.»

                  Бесспорно, расковырять сорцы glibc полезно, особенно, если разобраться, зачем они сделали именно так, но исследование проблемы методом чтения документации тоже не заняло много времени.
                    +9

                    Я тоже не настоящий разработчик на Си, и документацию я читать тоже умею. Вот только если код чужой — то может оказаться далеко не сразу очевидно, что нарушено именно это условие. Корректное сообщение об ошибке хотя бы в отладочной сборке надёжнее.

                      0

                      Как только прочёл предложение автора "Переменные last и next имеют тип uint32_t", сразу стало понятно, что проблема — в диапазоне чисел, осталось только пойти на https://en.cppreference.com/w/c/string/byte/isalnum за подтверждением. Минус бы автору за то, что специально до конца не раскрывал значение переменной next.

                  +2

                  А ещё статическому анализатору поможет, который распознает assert и радостно сообщит «value out of range». (Но ни cppcheck, ни scan-build из clang-tools-7 в Debian 9 assert не помог увидеть ошибку.)

                +2

                Хм, но почему простой assert кладёт на лопатки branch prediction? Вроде же наоборот, он должен без проблем им предсказываться...

                  0
                  Я погорячился. Не будет он класть branch prediction на лопатки, т.к. процессор быстро поймет, что этот assert при нормальной работе приложения не срабатывает никогда.

                  Но и прироста производительности от него не будет: с точки зрения процессора, это не assert, а обычное сравнения (лишнее сравнение), какой-то переход и 2 ветки кода за ним. Через несколько итераций он начнет угадывать на какую ветку будет выполнен переход до сравнения.
                    +4

                    Если правильно расставить подсказки (ЕМНИП, для старых процессоров есть префиксы, новые разделяют переходы вперёд/назад), то в правильной программе ошибок предсказания перехода на ассертах не будет в принципе.

            +5

            Тесты в инете показывают что реализация glibc быстрее: https://paste.sr.ht/%7Eminus/18b44cfe58789bc1fb69494130e859a1189d1772

              +6

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

                0

                С тестами всё нормально. Все упомянутые "навороты" в glibc, в том числе, для скорости.

                  +1

                  Надо ассемблерный код обоих вариантов смотреть. Ощущение, будто в случае с glibc функция заинлайнилась, а в случае с musl — нет, и такое сравнение нельзя назвать корректным.

                    +21

                    Реализация glibc избавлена от сравнений и условных переходов, в том числе изначально рассчитана на инлайнинг. Её примерно всегда дешевле заинлайнить чем вызвать, как по объему кода, так и по скорости.


                    В целом довольно наивно/глупо писать статью со словами "галиматья", "дурацкая херня" и т.п. (это я про автора, а переводчику респект) не понимая и 1/10 причин "зачем и почему" так сделано. А голосование довольно хорошо показывает "среднюю по больнице" квалификацию голосующих.

                      +3
                      А голосование довольно хорошо показывает «среднюю по больнице» квалификацию голосующих.
                      Судя по статьям про стандарт C++ можно и результаты голосования не открывать, так как там дофига комментариев типа «в моём Rust/Go/C#/Java/JavaScript/PHP всё понятно, такой фигни нет, дурачки из комитета/писатели компиляторов только и делают, что UB вводят».
                        0
                        В C/C++ вообще довольно много случаев с UB — я так понимаю, K&R и СтраусТруп сие пооставляли для каких-то неявно-неочевидных оптимизаций — однако, как правило, все равно куча народу стреляет себе в ногу )))
                      +4

                      Ну, собственно, потому в glibc и используются макросы вместо функций, что макросы гарантированно инлайнятся...

                    +6
                    Не две строки кода, а примерно 3 арифметические операции и 3 сравнения. Сравнения сбивают с толку предсказание переходов в процессоре и производительность падает.

                    Подозреваю, что glibc-шный метод, со всей его работой с локалями сводится к массиву из 256 элементов, для которых заранее рассчитано, являются ли они «alphanumeric».

                    Что-то вроде
                     static bool g_isAlphanumeric[256] = initAlphanumericOnce();
                     bool isalnum(unsigned int c) { return g_isAlphanumeric[c]; }

                    Массив быстро попадает в кеш процессора, лишние разыменования указателей нивелируются способностью процессора выполнять несколько микроинструкций за такт: никто не мешает процессору тянуть данные из этой таблицы заранее, и никто не мешает в процессе доступа к памяти посчитать что-нибудь заранее на АЛУ. Пока какой-то if() из кода по типу интуитивной реализации isalnum() не обдурит предсказатель переходов, и весь конвейер не будет сброшен и перезапущен.
                      +2

                      Я конечно тоже настоящий программист на C, но может лучше тогда так и было писать( с учётом локалей, если они могут повлиять на её результат )? Чтобы функция простой проверки не размазывалась на десяток макросов. Спасибо хоть на нестандартные расширения компилятора не полагается

                        +2
                        Сравнения сами по себе не обязательно сбивают с толку предсказание переходов. Если требуется только результат сравнения (как, например, в isalpha в musl), то инструкция перехода не нужна: вместо этого останется только cmp + добавится вытаскивание значений ZF/SF из регистра с флагами. Но зависит от реализации компилятора, конечно.
                        +5
                        Вдогонку: в наивной реализации используется арифметика: все эти вычитания и сравнения. В случае предварительно посчитанной таблицы, арифметики нет вообще, т.к. доступ по индексу для массива целых чисел еще с допентиумных времен перестал задействовать ALU.
                          +2
                          Так ещё с пентиумных времён арифметика быстрее предпосчитанных таблиц (например, в случае с 16-битным умножением). Не говорите только, что таблички влезают в L1. Если всё время на это полагаться, то тут 256 байт в L1, там 384 байта в L1, здесь 1024 байта, и L1 кончился.
                            0
                            Пруфы есть? Арифметика, как минимум, нагружает ALU.
                            256 байт в L1, там 384 байта в L1, здесь 1024 байта, и L1 кончился.
                            Речь идет о нагруженных циклах с кучей итераций.
                              0
                              Посмотрите, сколько стоит cache miss.
                              Речь идет о нагруженных циклах с кучей итераций
                              Нагруженный цикл не может быть в 1000 инструкций длиной? С кучей алгоритмов, которым всем надо либо память, либо арифметику?
                                +2
                                Я надеялся на примеры или доказательства, а вы просто беседуете со мной.

                                Обсуждался случай с обработкой текста, а не попытки засунуть 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%
                                  0
                                  Быстрее, пока влезает в L1. Разработчики библиотеки не знают, в каком алгоритме будет использоваться их isalnum, и сколько ещё таких жадных до кеша функций будет в цикле.

                                  В тестах и некоторых примерах, конечно, всё хорошо.

                                  Я не пытаюсь доказать, что арифметика всегда лучше. Я только хочу отметить, что не надо всё перекладывать на таблицы. Пусть и небольшие в каждой функции, в сумме они могут превысить предел L1.
                      +9
                      Фишка именно в мэппинге в локаль. Например, в некоторых языках есть такая штука, как умлауты (находятся в extended ASCII, 128-255). «Наивная» реализация не будет воспринимать их как буквы, хотя должна (в соответствующей локали). Так что гражданин ИМХО неправ.
                        +1

                        так он прямо об этом написал:


                        Теперь становится очевидно, как исправить проблему. Мой косяк. Оказывается, я не могу скормить в isalnum произвольный символ UCS-32 для проверки на его вхождение в диапазоны 0x30-0x39, 0x41-0x5A и 0x61-0x7A

                        а вообще от этих локалей одни проблемы. Хорошо бы выкинуть их к чертовой бабушке и везде пользоваться UTF-8.

                          +11

                          Не поможет, локаль — это не только кодировка, один и тот же Юникод в разных локалях может работать по-разному. И если на результат isalnum культура не влияет — то вот tolower от локали даже при использовании Юникода зависит.


                          Наиболее известный пример — турецкая локаль, с их знаменитыми символами İ и ı.

                            +8

                            Но такое ощущение, что он так и не понял, ЗАЧЕМ так сделано, и до конца продолжает нахваливать библиотеку с наивной (и не вполне корректной) реализацией isalpha() как образец для подражания. В общем, ИМХО, rant высосан из пальца.

                              +4
                              Все же допустимость UB для такой функции это перебор.
                                +6

                                С другой стороны, передавать char32_t или его аналог в функцию, которая принципиально работает только с char | EOF — значит вообще не понимать что делаешь.

                                  +2
                                  С третьей стороны, раз внутри функции массивы (с возможностью вылета за пределы) — почему не делать проверку на валидность входных данных?
                                    +1

                                    Ради скорости. Выше уже обсудили это.

                                      0
                                      Всё равно проверка нужна: мы же не хотим, чтобы от текста в не той кодировке программа падала. Только она будет самописная и не оптимизированная под все возможные архитектуры.
                                        +1

                                        А при чём тут кодировка? Функция не упадёт пока в неё передаётся char, какую бы кодировку этот char не использовал.

                                          0
                                          На входе у функции не char а int
                                                 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 на валидность, причем с учетом кодировки.
                                            0

                                            И что? Это же Си, тут много где int. К слову, для первых двух ваших вариантов isalnum работает не падая, а для последних двух она просто неприменима.

                                              0
                                              Это же Си, тут много где 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

                                                +1

                                                Что-то я не вижу в стандарте требования чтобы 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.
                                  +7

                                  Нет. Это парадигма С — Если что-то не по стандарту, то можно хоть диск форматировать. За счет этого отсутсвуют всякие проверки и код работает на порядок быстрее. Но программисту нельзя допускать ошибки, да.


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

                                    +3

                                    Где-то в мире загрустил один Торвальдс с историей про memcpy и memmove.

                                      +5
                                      Торвальдс стойко выдержал этот удар судьбы, даже не стал переписывать ядро и git на хаскель.
                                        +3

                                        Ну хоть раст в ядре разрешил.

                                        +1

                                        А для тех кто на бронетанке можно пожалуйста поподробнее про эту историю?

                                          +2

                                          memcpy предполагает неперекрывающиеся области памяти, memmove может работать и с перекрывающимися. Лет 10 назад в glibc поменяли реализацию memcpy, чтобы воспользоваться этим, предположительно, для увеличения производительности, в результате чего сломался Adobe Flash Player (и, возможно, другой софт, но про флеш было больше всего разговоров). В итоге Линус выразил своё фи.

                                            –1
                                            Спасибо. Я не особо знаток Линуса как человека, но похоже на какой-то плач обиженной девчонки. Кому надо тот пусть и фиксит (причем довольно просто) свой код следуя как раз неплохому предложению Линуса — в своем коде пусть и заводят макросню алиасищаю memcpy на memmov, а мне пожалуйста оставьте код который работает с УБ и по стандарту (да тому который он применяет по «назначению»), но хотя бы потенциально быстрее.
                                              0

                                              Насчет "потенциально быстрее" Линус в паре комментариев ниже подробно пояснил, почему это полная ерунда.

                                                0

                                                Линус распинался не о том что fail-fast плох или вреден, а что неприемлем переход на fail-fast "задним числом", когда под раздачу попадают ничего непонимающие детипользователи.

                                                  +2
                                                  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.
                                      –4

                                      +100500

                                      0
                                      Хорошо бы выкинуть их к чертовой бабушке и везде пользоваться UTF-8.

                                      Второй пункт прямо противоречит первому

                                    +3
                                    Ну так если надо написать оптимальный по производительности код, который будет быстрым на разных архитектурах и даже микроархитектурах, то приходится писать нечитаемую дичь. Увы, но даже для разных микроархитектур x86-64 нужно компилировать разную последовательность инструкций. В zen, вроде, 2 определённые инструкции могут превращаться в один uop. Когда-то cmov в большем числе случаев следовало использовать. Естественно, приходится давать компилятору как можно больше информации и давать такой код, который компилятор сможет для разных архитектур/микроархитектур максимально эффективно транслировать в машинные коды.
                                    Если вы посмотрите на эффективные алгоритмы умножения матриц, то там тоже будет плохочитаемый код. Увы, но красивый и быстрый код почти всегда антонимы. Важно, чтобы хотя бы интерфейсы были читаемы и просты.
                                      +3
                                      Я вот сразу догадался, что isalnum'у передают char с отрицательным значением.

                                      Такой вот у нас в C isalnum. Понять это нельзя, это можно только запомнить.
                                        +11
                                        Я просто обожаю иметь дело с локалями.

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

                                          0

                                          Я всегда недолюбливал локали. Теперь я знаю почему они так странно сделаны. Хотя это всё ни капли не извиняет их создателей. Спасибо.

                                          +2
                                          А где тут история библиотек? Реализая из musl выглядит как написанная за пять минут, под силу даже школьнику — понятно почему она выглядит именно так. А вот почему в glibc так написано — непонятно, явно преследовали какую-то цель, которую можно посмотреть по коммитам, и для этого точно требовались все эти средства. Но эта тема остаётся нераскрыта.
                                            +3
                                            А где тут история библиотек?

                                            Наверное правильнее было бы перевести как "рассказ о ..." (в оригинале a tale of).


                                            А вот почему в glibc так написано — непонятно, явно преследовали какую-то цель, которую можно посмотреть по коммитам, и для этого точно требовались все эти средства.

                                            Как минимум, эта реализация, в отличие от musl, поддерживает локали (что предписано стандартом).
                                            А столько макросов навёрнуто вероятно ради быстрой работы на всех поддерживаемых архитектурах.

                                              +2

                                              Традиционно "A Tale of Two Cities", к которому, вероятно, отсылает название оригинала, переводится как "Повесть о двух городах".

                                            +1
                                            Если я правильно помню, в MSVC как раз есть отладочный assert на то, что аргумент находится в допустимом диапазоне. Те, кто хоть раз «напоролся» — наверное, надолго запоминают про эту особенность функций ctype…
                                              +1

                                              В голосовалке не хватает опции "Кривой стандарт ISO C". Почему не бы дополнить описание функции isalnum тем, что на всех остальных значениях возвращать false?

                                                0
                                                Потому что будет медленнее.
                                                  0
                                                  Это понятно. Однако в 2020-м году в разработке ПО уже другие приоритеты, поэтому лично я бы проголосовал за предложенный мною пункт.
                                                    +4
                                                    У кого другие приоритеты, тот в 2020-м году будет писать не на Си.
                                                      0
                                                      Но стандарт принимается не только для тех, кто программы пишет, но и для тех, кто этими программами пользуется. Я, как потребитель C-шных программ, голосую за п. 4.
                                                        +2

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

                                                    0

                                                    И сразу медленнее! Нашли как выкрутится сейчас — найдут и как выкрутиться потом.

                                                      +2
                                                      Мне нравится комментарий в libc:
                                                      ISO C требует, чтобы функции ctype работали
                                                      со значениями типа `unsigned char' и EOF; мы также поддерживаем отрицательные
                                                      значения `signed char' для совместимости со старыми некорректными программами

                                                      То есть, чуть-чуть, но они отступают от принципа «никаких компромисов». Добавили лишних 768 байт в таблицах, чтобы некорректный код не падал.
                                                    +1
                                                    Автор нагнетает за счет интригующего расследования по поиску реализации функции. Но этот поиск делается за минуту с помощью опций gcc --save-temps -C. После запуска компиляции с этими опциями образуются файлы с расширением .i, в которых все будет уже на блюдечке подготовлено. Ковыряться в исходниках glibc необязательно.
                                                      +1
                                                      Может быть, так бы он быстрее разобрался, но чтобы подготовить материал для статьи, недостаточно вывалить портянку препроцессированного кода. Нужно показать, что откуда взялось.

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

                                                    Самое читаемое