четверг, 1 мая 2014 г.

Про оптимизации, безопасность и "нормальные языки".

В конце 2013 года вышла довольно интересная статья "Towards Optimization-Safe Systems: Analyzing the Impact of Undefined Behavior" про то как компиляторы, применяя различные агрессивные оптимизации убирают проверки безопасности. Хочу поделиться своими мыслями на этот счёт.
Во-первых часть той статьи я перевёл на русский (остаток переводить лень, но если кому-то нужно готов продолжить). Команда, написавшая эту статью весьма интересная - у них есть и более ранние работы, посвящённые неопределённому поведению и ошибкам, к которым оно приводит. Очень советую почитать, вот пара названий их статей: "Linux kernel vulnerabilities:
State-of-the-art defenses and open problems", "Undefined Behavior: What Happened to My Code?", по ссылкам там можно и массу других любопытных статей найти.

Если кому-то лень читать статью целиком, то в двух словах суть сводится к следующему: если Вы пишете код, который приводит в UB (undefined behaviour), то готовьтесь к неприятным последствиям. Например:

char *buf = ...;
char *buf_end = ...;
unsigned int len = ...;
if (buf + len >= buf_end)
    return;
 /* len too large */
if (buf + len < buf)
    return;

Здесь вторая проверка будет удалена, т.к. компилятор уверен, что buf + len не может быть меньше buf, ведь в противном случае было переполнение, приводящее к UB, а программист умный и ну никак не мог привести свою программу в такое состояние.

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

Как компиляторщик, который сам разрабатывает подобные оптимизации могу сказать, что программисты сами виноваты, надо было стандарт читать здесь довольно глубокая концептуальная проблема. Стандарт Си, например, содержит массу мест, где в двух строках довольно расплывчатым языком описывается какое-либо недопустимое действие. Об этой паре строчек знает полтора человека, которые потерев руки коммитят в компилятор оптимизацию, использующую данную возможность. А потом кто-то случайно обнаруживает разное поведение на первый взгляд корректного кода в режимах -O0 и -O3. Отлаживать такие штуки - адский ад. Поэтому возникает вопрос - а стоит ли делать оптимизации, которые ломают вообще весь софт ради пары процентов (это если повезёт) прироста производительности?

Формально, конечно, правы компиляторщики. И более того, разработчики компилятора обычно делают warning'и, которые предупреждают об опасности. Более того в gcc-4.9 появился специальный санитайзер: -fsanitize=undefined, отлавливающий потенциально опасные конструкции. Да, понятно, что все опасные случаи, которыми воспользуется компилятор отловить невозможно, но к сожалению, не все компилят даже с ключом -Wall, про -Werror даже говорить не приходится, поэтому программисты в любом случае попадают в истории.

Мне как-то высказывали мнение, что в "нормальных языках" такая ситуация невозможна в принципе. Я посмотрел в стандарт Haskell - в соответствии с ним компилятор имеет полное право убрать вторую проверку из нашего примера, хотя на практике мне этого добиться не удалось. Думаю, компилятор до этого ещё не дорос ;) Я также посмотрел в стандарт Erlang, но там вообще ничего про переполнение не сказано и хотел посмотреть в стандарт OCaml, но как бы это сказать... Нет у OCaml стандарта! Да, они в качестве стандарта используют книгу по OCaml, но понятно, что нельзя сделать что-либо серьёзное по документу который обновляют раз в несколько месяцев.

Лично мне кажется, что такие оптимизации делать нужно в любом случае, ведь никогда не знаешь когда 2% ускорения превратятся в 20% из-за наложения эффекта нескольких оптимизаций. Вопрос безопасности... Ну для начала пусть программисты перестанут игнорировать предупреждения компилятора, а там уже о чём-то рассуждать можно будет. Если нужна сверх надёжность, либо -O0, либо качественный код и качественное тестирование. К сожалению, в российских гос. шарагах хорошие программисты - редкость и там проблемы возникают не из-за коварного компилятора, а из-за банального говнокода.

Комментариев нет:

Отправить комментарий