Pull to refresh

Comments 38

А значит, черт лысый не знает, что творится в наших программах. Хотелось бы точно знать, когда вызов с поздним связыванием, а когда на стадии компиляции все уже известно. Но почтенный возраст сказывается на С++…
Jon Blow пилит язык, где позднее связывание делается пока вручную, а в перспективе макросом или в этом роде. Выглядит многообещающе.
А в Rust можно достаточно просто определить, когда происходит связывание. Если используется параметрический полиморфизм — раннее, ad hoc — позднее связывание. Ну или могли уже это изменить — летом так было :)
Простите. а зачем вам знать, поздние связывание или на стадии компиляции все известно?
Два лишних указателя не всегда проходят бесследно. В играх, особенно под мобильные платформы, при оптимизации иногда приходится жертвовать уровнем абстракции ради производительности, и знание, когда именно отказ от абстракции поможет производительность улучшить, дорогого стоит, потому что пробовать перебирать миллионы вариантов и делать для них бенчмарки может быть слишком трудозатратным.
Знать, где точно нет обращения к vtable, сужает круг возможных направлений уродования кода.
Ну давайте подумаем. наследование с переопределением виртуальных методов позволяющий программисту не знать, что конкретно он вызывает.

То, что компилятор разобрался — то честь ему и хвала. Но следующее движение Автора может все в корне поменять
Тьфу, не подумал о том, что с помощью темплейтов в С++ можно точно так же, как в rust (а точнее, очевидно, наоборот — в rust как в С++) гарантировать раннее связывание. Ну, с поправкой на разницу между темплейтами и генериками в rust.
Это — то, что меня волновало, а не предсказание оптимизации dynamic dispatch, как вы могли подумать. Извините, что взбаламутил :)
Если вы сами знаете на этапе компиляции, что будет когда вызвано — используйте статический полиморфизм, а не динамический, он гарантирует отсутствие позднего связывания
Вы правы, поспешил, людей вот насмешил :)
Интересно. Для полноты картины я бы добавил, что в том подавлящем числе случаев, когда виртуальная функция вызывается косвенным вызовом через таблицу, в дело вступают оптимизации в железе.

Такие косвенные вызовы неожиданно хорошо предсказываются.
Предсказание вызова во время работы программы — это хорошо, но процессор вынужден работать с машинным кодом, который можно было бы упростить, используя знания о том, каким конструкциям языка высокого уровня он соответствует. Вот например:
class Derived : public Base {
public:
     virtual int Magic() { return 100500; }
};

void someFarAwayFunction()
{
    Base* base = new Derived();
    std::vector<int> somethingUseful;
    //blahblahblah, fill vector
    { // this block can be removed completely
        int uselessSum = std::accumulate( somethingUseful.begin(),
             somethingUseful.end(), 0);
        if( base->Magic() < 0 ) {
             printf( "%d", uselessSum );
        }
    }
}


Если компилятор может понять, что здесь гарантированно вызывается именно Derived::Magic(), которая возвращает положительное число, он может прийти к выводу, что вызов printf() никогда не выполняется, а из этого следует, что можно попробовать удалить и код вычисления параметров для этого вызова. В результате весь внутренний блок можно удалить, и тогда он просто не дойдет до процессора.
Здесь тоже работает предсказание ветвлений.

Если встраивать тело функции вместо вызова возможностей для оптимизации больше — абсолютно согласен.
Нет, с компилятором все в порядке, но агрессивность оптимизации отрицать бессмысленно.
А меня, честно говоря, этот момент смущает. Насколько я понимаю, ассемблерный вывод идет на уровне получения объектного файла, а не готового исходника, и в этот момент компилятор ещё не может знать, что у нас не будет других единиц трансляции. Суть в том, что в любой единице трансляции мы можем подменить глобальные operator new() и operator delete() на свою реализацию, и компилятор будет обязан использовать эту реализацию для всей программы. Поэтому то, что он выкинул здесь new и delete, а, соотвественно, и вызовы этих глобальных операторов, может изменить наблюдаемое поведение.
Переопределение new и delete будет в заголовочном файле, который препроцессор подставит в код данной единицы трансляции. Никаких «внезапно» при компиляции не бывает.
Либо я плохо объяснил, либо вы неправы. Создайте такие два файла:
operators_replacement.cpp
#include <iostream>
#include <cstdlib>

void* operator new(std::size_t count)
{
	std::cout << "Allocation\n";
	return malloc(count);
}

void operator delete(void* ptr)
{
	std::cout << "Deallocation\n";
	free(ptr);
}

main.cpp
int main()
{
auto ptr = new int;
delete ptr;
}
Соберите их вместе в один исполняемый файл и посмотрите на вывод. Не уверен, что этот код на 100% безопасен, т.к. он полагается на то, что std::cout не использует ::operator new() для своей инициализации (иначе произойдет запись в ещё неиницилизированный поток), но на gcc это работает. Разумеется, можно делать что-то другое (например, глобальную переменную-счетчик крутить) и тогда этой проблемы не будет.
Так и есть, все, что может быть вызвано извне, подставляется в виде символа, которые потом уже соберет линковщик. Именно поэтому если вы хотите применять инлайны, они должны быть доступны в момент компиляции. Хотя есть возможность инлайнить и в момент линковки.

Мне кажется, этот код будет на 100% безопасен, поскольку std::cout либо полагается на то, что код оператора new для библиотеки уже где-то определен и скомпонован (если библиотеки run-time), либо что будет использован доступный компоновщику код в случае static-сборки.
либо что будет использован доступный компоновщику код в случае static-сборки.
Проблема в конкретной реализации оператора new: если std::cout вызовет его в процессе своей инициализации, то это приведет к записи в ещё неинициализированный поток. Но вопрос не в этом, а в том, почему clang выкинул вызов этих операторов из сгенерированного кода, он вроде как не мог во время генерации знать, сделаем мы такое переопределение или нет.
А, да, вы правы про безопасность.

Пытаюсь придумать адекватное объяснение и не могу придумать ничего действительно обоснованного. Такое ощущение, что компилятор за нас решил сделать функцию инлайном, и оптимизировал ее вызов вместе с созданием объекта. Интересно бы было собрать ваш код с оператором clang'ом. И я могу предполагать, что в случае с раздельными модулями, результат бы был похож на gcc.
Это же девиртуализация :-) для помощи компилятору стоит активно использовать final, но он и сам неплохо справляется.
Это круто, конечно, но вот в местах, где критична производительность, не хочется отдавать всё на волю компилятора.
Было бы, наверное, круто иметь какое-нибудь ключевое слово чтобы заставить компилятор принудительно вставлять конкретный вызов функции вместо vtable.
PS
А что будет творится с инлайнингом виртуальных функций если начнется пляска с исключениями?
Код вида
static_cast<concrete_type*>(base_ptr)->function(args...)

может заставить компилятор использовать функцию конкретного типа :-)
Если функция или класс не final, то нет, вдруг base_ptr указывает не на concrete_type, а на производный от него тип c переопределенной в нем function.
Кончено, гарантий нет, я поэтому и говорю «может». Но вообще я в уме держал что «наследоваться от конкретных типов — плохо, поэтому concrete_type final». :-)
Но, в некоторых случаях компилятор может и сам проследить, что concrete_type является листовым типом в иерархии.
Если точно уверен, что base_ptr указывает именно на concrete_type, то
static_cast<concrete_type*>(base_ptr)->concrete_type::function(args...)
Это делается несколько иначе. Вместо

static_cast<concrete_type*>(base_ptr)->function(args...)


нужно написать

(*base_ptr).concrete_type::function(args...)


Тогда будет вызван именно метод
concrete_type::function(args...)
, вне зависимости от того, является ли base_ptr производным классом или нет.
Такой код работает только для upcasting'а, можно вызывать функцию одного из типов выше по иерархии (базовых), но не ниже.
Финальным корректным вариантом видится такой код:
static_cast<concrete_type*>(base_ptr)->concrete_type::function(args...)
Когда тип известен, и дело только за кастом, то лучше
boost::dynamic_cast<Derived>(x)
По-моему, это отнюдь не было бы круто. Это не нужно. Если вы знаете, что здесь по указателю Base* лежит Derived* — ну так и напишите Derived* (хотя бы приведите с помощью static_cast).
Это не работает, как способ сказать компилятору «по указателю лежит Derived», это говорит компилятору лишь «по указателю лежит Derived или производный от него тип». Это работает только если Derived объявлен как final, тогда компилятор понимает, что производных типов быть не может, и девиртуализирует вызов.
Для таких ситуаций, когда хочется полного контроля и максимальной производительности, и были придуманы шаблоны.
В той же STL очень мало виртуальных функций.
Приведенные примеры простые. Намного интереснее получается, компилятор девиртуализирует вызовы во время LTO. Такие случаи уже сложно руками оптимизировать.
А представляете, в Java все instance-вызовы виртуальные, вся надежда только на компиляторы. :)
Если кому-то интересно почему виртаульные методы в классическом виде сказываются на производительности можно посмотреть это длинющее видео: channel9.msdn.com/Events/GoingNative/2013/Compiler-Confidential (с 25 минуты вроде разговор заходит на эту тему).
Слишком простые примеры.
Вот если взять 100500 классов со сложной иерархией и распихать их по нескольким библиотекам, скомпилированным разными версиями компилятора вот тогда получится весело
и никакая LTO не поможет.

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

Способ гарантированно получить от компилятора конкретные инструкции на выходе — написать asm volatile("") и вперед с песней.

Но редко кто может похвастаться умением писать на ассемблере лучше, чем это делает GCC.

Второй способ — вдумчивое изучение всех опций и #pragma компилятора. А потом тесты, тесты и снова тесты.
Спасибо за статью! Написано восхитительно, приятно читать.

Вот только один момент, связанный с подсветкой синтаксиса. После слов «Здесь и далее будем использовать MinGW с gcc 4.9.0» есть кусочек кода с вызовом компилятора и objdump, который почему-то подсветился как brainfuck. В результате в Safari в режиме чтения он отображался так:
++ - - - . .
 - -  - ----- . >.

Я вначале подумал, а не является ли это пасхалкой — даже интерпретатор поставил, но ничего вразумительного он не показал :)
Интересно получилось. При верстке был использован тег <source> без атрибута lang. При просмотре «инспектором» Огнелиса было видно, что в HTML страницы оказался элемент <source lang=«brainfuck»> Прописал lang=«bash».
Sign up to leave a comment.