Почему подсказки компилятору сделаны неправильно

Для написания быстрого ПО недостаточно просто реализовать хороший алгоритм на быстром (Си) языке. Для направленной оптимизации компилятору мало просто видеть код, он должен понимать те его свойства, которые язык выразить не в состоянии. В современных компилляторах gcc и llvm описание таких свойств задаётся при помощи атрибутов или указания #pragma. Однако здесь я хочу рассказать почему такая схема взаимодействия пользователя с компилятором в корне неверна.

Рассмотрим простой пример. Атрибуты pure или const. Эти атрибуты говорят компилятору что функция не имеет побочных эффектов (попробуйте по названию угадать разницу между ними). Пользователь может выставить этот атрибут в описание функции или в её определение. Когда компилятор видит этот атрибут, он понимает, что функция «хорошая», что её можно переставлять местами с другими функциями, и что результат для одинаковых параметров всегда будет одинаковым. А теперь представим что пользователь написал следующий код:

int A;

__attribute__ ((const))
int f()
{
  return ++A;
}

int main()
{
  int a;
  a = f();
  a = f();
  a = f();
  a = f();
  printf("%d\n", a);
}

Здесь функция, содержащая явный побочный эффект нарушает данную пользователем гарантию о его отсутствии. Предупреждения компилятор даже не вывел (вроде бы оно в принципе существует, но не входит в набор -Wall). Можно поиграть в угадайку «что выведет компилятор», ответ по ссылке.

Существует, например, набор атрибутов и встроенных функций (уже третий вид взаимодействия с компиляторами, заметили, да?), которые сообщают компилятору профильную информацию: вероятности переходов по дугам, горячие/холодные функции, количество итераций циклов. Они, хотя, и не ведут к ошибкам исполнения, однако вполне себе могут не соответствовать реальному профилю. И что тогда делать: опираться на реальный профиль или на подсказку, оставленную пользователем? А почему? А может свалиться с ошибкой?

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

Но это то что касается подхода к подсказкам в gcc/llvm. В golang ударились в другу крайность. Они вообще всячески избегают внесения в язык подобных вещей. Например, они отказались добавлять подсказку //go:inline (и правильно сделали) чтобы пользователь лишний раз не напрягал мозги. А вызов предподкачки у них доступен только для пакета internal, который находится в недрах компилятора (сделан prefetch у них, к слову, не очень). Т.е. они понимают проблему неправильных подсказок и заранее ампутируют саму такую возможность, что в целом вариант, но не для языков, которые претендуют на высокую производительность.

А как тогда сделать правильно? Это хороший вопрос, на который у меня есть идея, но нет ответа. Мне нравится атрибут stack_protect в gcc, который защищает функцию от выделения переменных на стеке. Если же хоть какая-то из переменных оказалась на стеке, то компилятор сломается. Таким образом пользователь лишён возможности ошибиться в назначении свойства: в этом случае компилятор ломается. Компилятор лишён возможности создать код, не соответствующий описанию. Единственное слабое место такого подхода - нужен довольно сложный механизм обратной связи от компилятора с пояснением почему произошло именно так, а не иначе и как это можно исправить. И вот тут то и будут основные хлопоты со стороны разработчика компилятора.