46 subscribers
11 links
Канал посвященный моим попыткам освоить C++ и немного компиляторы. Буду сюда кидать интересные для меня вещи.
Download Telegram
Channel created
Channel photo updated
https://en.cppreference.com/w/cpp/filesystem/exists

Когда только начинаешь учить С++ после высокоуровневых языка самое забавное — насколько иногда простые вещи в этом языке сложно исполнять.
К примеру функция проверки существования файла на диске в стандартную библиотеку была добавлена только в стандарте 2017го года. До этого приходилось извращаться со всякими:

ifstream f(name.c_str());
return f.good();
Еще одна проблема с переходом на низкоуровневые языки — операции с указателями (pointer).

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

Рассмотрим следующий код:
vector<int> v;
v.push_back(1);
cout << v[0] << " ";
int *p = &*(--v.end());

(*p) = 2;
cout << v[0] << " ";
(*p) = 3;
cout << v[0] << " ";

Данный код выведет на экран значения:
1 2 3

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

Давайте немного поменяем исходный код:
vector<int> v;

v.reserve(1);

v.push_back(1);
cout << v[0] << " ";
int *p = &*(--v.end());

(*p) = 2;
cout << v[0] << " ";

v.reserve(3);

(*p) = 3;
cout << v[0] << " ";

В лучшем случае эта вариация выведет на экран: 1 2 2, так как мы запишем последнее изменение в область памяти уже не имеющей отношения к вектору.

В худшем же случае данный код просто упадет в попытке произвести запись в ненадлежащий сегмент.
Количество способов получить undefined behavior в С++ порой поражает.
Недавно я наткнулся на, кажется, самый идиотский из возможных способов которым хочу поделиться.

Допустим мы написали следующий код:

#include <iostream>

struct Person {
char cityInfo[4];
};

struct City {
long id;
};

int main() {
Person me;
City *city = (City*) &me.cityInfo;
std::cout << city->id << std::endl;
return 0;
}

Все казалось бы очевидно: у нас есть структура с информацией о каком то человеке (Person), частью этой информации является байт массив с информацией о городе в котором человек живет (cityInfo). Все что мы хотим сделать в данном коде — интерпретировать байт массив города в виде структуры (City) и вывести на экран идентификатор этого города.
Казалось бы какие тут могут быть проблемы? И действительно структура города имеет размер sizeof(long), что в любой ситуации меньше sizeof(char[4]), а следовательно мы спокойно можем интерпретировать массив соответствующим образом.
И действительно в данном коде проблем нет, давайте попробуем скомпилировать его с соответствующим санитайзером и убедиться в этом:
 clang++ -fsanitize=undefined  good_sample.cc  && ./a.out


Теперь давайте добавим в структуру с человеком информацию о его возрасте:
struct Person {
int age;
char cityInfo[4];
};

Если же мы теперь попробуем перекомпилировать пример и выполнить его мы получим следующую ошибку:
clang++ -fsanitize=undefined  ub_sample.cc  && ./a.out
ub_sample.cc:17:24: runtime error: member access within misaligned address 0x7ffd07a42df4 for type 'City', which requires 8 byte alignment
0x7ffd07a42df4: note: pointer points here
f0 2e a4 07 fd 7f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 83 70 31 84 a6 7f 00 00
^
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ub_sample.cc:17:24 in
ub_sample.cc:17:24: runtime error: load of misaligned address 0x7ffd07a42df4 for type 'long', which requires 8 byte alignment
0x7ffd07a42df4: note: pointer points here
f0 2e a4 07 fd 7f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 83 70 31 84 a6 7f 00 00
^
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior ub_sample.cc:17:24 in
32765

В чем же тут проблема? А проблема тут возникает в выравнивании памяти (alignment).
Дело в том, что примитивные типа должны записываться в ячейки с адресом значение которого целочисленно делится на выравнивание соответствующего типа.
Возьмем для примера long — выравнивание данного типа равно 8 байтам (на архитектуре x86-64), это значит что значение типа long должно начинаться с адресов делящихся на 8 (0, 8, 16, 24, etc). Тем не менее в мире С++ никто не останавливает нас от интерпретации любого случайного адреса памяти как long. Однако подобная интерпретация собственно и приводит к undefined behavior. Дело в том, что выравнивание памяти важно при низкоуровневых операциях, таких как подкачка кэша процессора. Соответственно неверное выравнивание может привести в дальнейшем к ошибкам в вычислениях.

Что же произошло в нашем случае?
В изначальной структуре long значение идентификатора города (в виде массива cityInfo) находилось в ячейке со смещением 0, что подходит под условия выравнивания. Однако после добавления int свойства (с размером в 4 байта), массив с информацией о городе сместился в ячейку 4, что собственно и привело к UB.

Что можно сделать в данном случае?
Мы можем вручную попросить компилятор использовать заданное смещение для свойства в структуре:
struct Person {
int age;
char cityInfo[4] __attribute__ ((aligned (8)));
};

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


А что если мы не хотим тратить лишнюю память?
Продолжение.

В таком случае нам на помощь приходит std::bit_cast (https://en.cppreference.com/w/cpp/numeric/bit_cast), либо его аналог написанный вручную:
#include <cstdint>
#include <cstring>
#include <iostream>

struct Person {
int age;
char cityInfo[4];
};

struct City {
long id;
};

int main() {
Person me;
City city;
std::memcpy(&city, &me.cityInfo, sizeof(City));
std::cout << city.id << std::endl;
return 0;
}

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

Все примеры вы можете найти тут: https://github.com/PatriosTheGreat/cppchannel/tree/main/ub_cast
Грабли со static_cast

Недавно наткнулся на забавную багу в коде, которая заставила задуматься над поведением, казалось бы банального, static_cast.
Давайте рассмотрим следующее дерево наследования:

struct Base {};
struct ActualChild : Base {};
struct OtherChild : Base {};


Давайте теперь сделаем static_cast между братскими классами:

ActualChild obj;
ActualChild* actual_ptr = &obj;
Base* base_ptr = actual_ptr;

OtherChild* other_ptr = static_cast<OtherChild*>(base_ptr);


В теории static_cast не должен делать ничего специфичного. Он просто интерпретирует память как объект данного типа. Пока что вроде особых проблем нет, но давайте добавим в класс OtherChild метод и попробуем его вызвать:

struct OtherChild : Base {
int foo() { return 0; }
};

other_ptr->foo();


Полный код доступен тут: https://godbolt.org/z/Yvr5oMd7h

Странным образом результатом такого вызова будет вполне корректный возврат значения 0. Впрочем если у вас опыта с плюсами побольше моего – для вас ничего странного в этом нет. Компилятор просто вставил вызов метода OtherChild::foo() и передал ему соответствующий указатель в качестве свойства this. Это можно и увидеть в окне с ассемблером в следующих строках:
 mov     rdi, qword ptr [rbp - 32]
call OtherChild::foo()

Тут логично возникает два вопроса:
1. Что будет если метод foo станет виртуальным?
2. Что будет если метод foo решит использовать свойства класса OtherClass.

Ответ на первый вопрос я для краткости заметки отложу на следующий раз, а сейчас давайте попробуем использовать какое-нибудь свойство:

struct OtherChild : Base {
int foo() { return value; }


int value;
};


other_ptr->foo();

Полный код: https://godbolt.org/z/raccns5hs
Результатом подобного вызова будет считывание случайного куска памяти. Чтобы понять как это работает давайте посмотрим на ассемблер:

mov     rax, qword ptr [rbp - 8]
mov eax, dword ptr [rax]


В данном случае в rbp лежит указатель на объект класса ActualChild (this). Компилятор же в данном участке думает что он работает с OtherChild. Для этого класса он знает, что значение value лежит в первых 4 байтах.
По сути передав в этот метод объект класса ActualChild мы просто считаем память лежащую в первых 4 байт от этого указателя. Давайте собственно проверим так ли это следующим кодом:

struct ValueHolder {
int value = 42;
};

int main() {
ValueHolder holder;
OtherChild* other_ptr =
static_cast<OtherChild*>(
(void*)&holder);
return other_ptr->foo();
}


https://godbolt.org/z/b561Yajqa

И действительно результатом данной программы будет 42.

В следующий раз я попробую поиграться с виртуальными методами и посмотреть как компилятор будет вести себя в этом случае. А в качестве вывода статьи скажу, что если вы не уверены что лежит под указателем на родительский класс – пользуйтесь dynamic_cast, хоть это и дороже и требует RTTI.
Продолжение граблей со static_cast
В прошлый раз мы выяснили что при касте к братскому классу компилятор вставит вызов метода того типа которым помечен объект.

Рассмотрим данный код:
struct Base {};
struct ActualChild : Base {
int foo() { return 1; }
};
struct OtherChild : Base {
int foo() { return 2; }
};


int main() {
ActualChild actual;
OtherChild* other_ptr = static_cast<OtherChild*>((Base*)&actual);
return other_ptr->foo();
}

https://godbolt.org/z/E1z194zf6

Результатом программы будет 2 и в окне ассемблера мы видим что компилятор вставил напрямую вызов OtherChild::foo() (9я строка).

Давайте теперь посмотрим что будет в случае если метод станет виртуальным. Для этого нам надо поменять только структуру Base:
struct Base {
virtual int foo() = 0;
};

https://godbolt.org/z/1Kh8qfcfs

Теперь результатом будет 1.
Но каким образом компилятор определил какой метод вызвать? Ответ на это мы найдем в окне ассемблера в следующих строках (7-12 строки):
        call    ActualChild::ActualChild() [base object constructor]
lea rax, [rbp - 16]

call qword ptr [rax]


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

Давайте попробуем понять какой именно адрес лежит в этой строке. Как видно из второй строчки – адрес достается из регистра rbp со смещением в 16. На всякий напомню что регистр rbp используется для хранения указателя на текущую голову стека. То есть конструктор ActualChild положил в стек указатель на соответствующую функцию foo. Давайте найдем как он это сделал. Для этого посмотрим на ассемблер данного конструктора (строки 16-30):

ActualChild::ActualChild() [base object constructor]:

call Base::Base() [base object constructor]

lea rcx, [rip + vtable for ActualChild]
add rcx, 16
mov qword ptr [rax], rcx
add rsp, 16
pop rbp
ret

Мы видим до 23й строки определенные операции которые нас не сильно интересуют. Далее мы видим вызов конструктора базового класса (строка 23). Затем в инструкции lea rcx, [rip + vtable for ActualChild] мы видимо, что в регистр rcx кладется указатель на глобальный объект vtable_for_ActualChild (rip это специальная инструкция конкретно для Intel которая позволяет брать адреса объектов в глобальной области).
После этого к адресу добавляется смещение в 16 (26 строка) и получившийся адрес собственно и кладется на стек, откуда мы его далее и получаем.

Зачем компилятору нужно смещение в 16? Для ответа на этот вопрос давайте собственно посмотрим что такое виртуальная таблица vtable for ActualChild в глобальной области. Для этого прокрутим ассемблер вниз до 48й строки где видим следующее:
vtable for ActualChild:
.quad 0
.quad typeinfo for ActualChild
.quad ActualChild::foo()

Итак в первых 8 битах лежит 0 (честно говоря не знаю зачем, возможно это результат отсутствия оптимизаций, я собирал с O0). Далее лежит некоторая RTTI информация о классе, и после этого на расстоянии в 16 бит начинается список указателей на непосредственно виртуальные методы. Таким образом мы получаем указатель на ActualChild::foo() который конструктор ActualChild положит в сам объект и который будет далее вызван компилятором.

Дописав эту заметку, я вдруг понял, что по сути она не имеет ничего общего со static_cast. Я просто разобрал как работают таблицы виртуальных методов. Извиняюсь за небольшое вранье в названии.
Битовые флаги

Недавно наткнулся в одной библиотеке на интересный способ объявления и использования битовых флагов которым хочу поделиться. Но перед тем как перейти к нему хотел бы показать как я бы лично их реализовал и чем мой метод хуже.

Для начала определение – под битовыми флагами я имею ввиду именованные свойства с которыми можно проводить битовые операции (побитовое И, побитовое ИЛИ и т.д). Определение наверно только запутало давайте лучше к примеру как я бы это реализовал:

struct File {
int permissions;
enum class ACLs {
read = 1,
write = 2,
remove = 4,
rename = 8
};
};


int main() {
File file;
file.permissions = (int)File::ACLs::read | (int)File::ACLs::write; // Файл в который можно читать и писать но не переименовывать или удалять


if (file.permissions & (int)File::ACLs::remove) { // Удалить файл если есть права на удаление
// removeFile(file);
}
}

https://godbolt.org/z/c9fevjEYG

Как видно из примера такие флаги очень удобно комбинировать, проверять на наличие свойства и т.д. Ну впрочем вы и сами явно в курсе. Давайте лучше сфокусируемся на недостатках этого кода. А их много:

1. Необходимо кастовать значение enum к int перед каждой битовой операцией: (int)File::ACLs::read | (int)...
2. Необходимо корректно инициализировать значения enum степенями двойки.
3. Битовые выражение для проверки наличия свойства которые сложно читать

А теперь собственно глянем как подобное может быть реализовано удобнее:
struct File {
union {
int permissions;
struct { int read:1, write:1, remove:1, rename:1; };
};
};


int main() {
File file;
file.permissions = 0b11; // Установить биты отвечающие за read и write


if (file.remove) { // Удалить файл если есть права на удаление
// removeFile(file);
}
}

https://godbolt.org/z/TxxEs71hP

На всякий если кто-то не понял что произошло объясню. Union позволяет объявить несколько альтернативных способов доступа к одному и тому же куску памяти. В данном случае первое свойство структуры File будет иметь размер Int (самый большой тип под Union), но при этом эту же область памяти можно будет в коде интерпретировать как второй заданный тип юниона. При этом второй тип в свою очередь состоит из 4 значений каждое из которых занимает ровно 1 бит памяти.
Таким образом мы можем одновременно использовать кусок памяти выделенный под свойство permissions и как целочисленное число (что позволит нам проводить битовые операции) и как набор битовых полей, что упростит нам проверку на наличие какого-то свойства.

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

Все вы в курсе, что создатели С++ посчитав свой язык слишком простым к освоению, решили добавить в него парочку, так сказать, интересных возможностей. Одной из таких возможностей является перегрузка операторов. Все мы знаем, что в отличи от той же Java, эта фича дает возможность C++ разработчикам писать свои удобные к использованию коллекции, умные указатели и многое другое. Но есть парочка операторов, которые в здравом уме, скорее всего, вы бы даже использовать не стали, но оказывается их вполне себе можно перегрузить. Давайте глянем на пару из них.

Для начала давайте глянем на операторы доступа к свойствам объекта. К таким операторам относятся оператор разыменования (*), взятие ссылки на (&), или доступа к свойству по адресу (->). Вы скорее всего знаете что эти операторы перегружаемые, что дает возможность реализовывать простые к использованию умные указатели. Однако один оператор меня тут удивил – оператор разыменования свойства класса (->*). Согластно cppref (https://en.cppreference.com/w/cpp/language/operator_member_access) В обычной жизни (без перегрузки) этот оператор равносилен конструкции (*object).*field_ptr. Тут лично у меня возник логичный вопрос, а зачем собственно разыменовывать свойство объекта?
Казалось бы, если у нас есть некоторое свойство класса которое является указателем в память, то его разыменование должно происходить так: *object->field_ptr. И я был абсолютно прав, однако оказалось что в С++ существует такая концепция как указатели на свойства класса. Давайте рассмотрим что это такое на следующем коде:

class A {
public:
A() { field = 0; }
int field;
};


int main() {
A a;
A *a_ptr = &a;


int (A::*field_ptr); // Вот это собственно указатель на свойство
field_ptr = &A::field; // А теперь оно указывет на свойство field
}


Вот собственно что бы пользоваться такими указателями и существует оператор ->* который можно использовать как:
return a_ptr->*field_ptr;


Потрясающе. И зачем мне перегружать этот оператор? Честно говоря я без понятия. На просторах интернета я нашел только этот вразумительный случай использования: https://stackoverflow.com/questions/2696864/are-free-operator-overloads-evil. Для простоты статьи я не буду вдаваться в объяснение, если вам интересно, почитайте первый же ответ описывающий библиотеку Boost.Phoenix. А мы двинемся дальше.

Операторы сравнения. Опять же, очевидно что можно перегрузить операторы <, >, =, но какого было мое удивление узнать, что начиная с С++20 существует оператор трехстороннего сравнения <=>. Давайте для начала поймем что он делает без перегрузок:

https://godbolt.org/z/1qs14eeoz
  auto comp = 1 <=> 2;
std::cout << typeid(comp).name() << std::endl;
std::cout << "Is lower: " << (comp < 0) << std::endl;
std::cout << "Is bigger: " << (comp > 0) << std::endl;
std::cout << "Is equal: " << (comp == 0) << std::endl;

Результат этого кода следующий:

std::strong_ordering
Is lower: 1
Is bigger: 0
Is equal: 0

Итак оператор <=> возвращает объект некого типа std::strong_ordering либо std::weak_ordering который можно сравнить с 0 операторами <, >, ‘==’ и который по сути реализует метод int compare. По сути перегрузка данного оператора позволяет нам одним методом реализовать целых 5 операторов: ==, !=, <=, >, >=.

Вот так может выглядеть класс двухмерных координат с использованием данного оператора:

https://godbolt.org/z/6W9fzKxcE
#include <compare>
#include <iostream>


struct Point {
int x, y;

friend constexpr std::strong_ordering operator<=>(Point lhs, Point rhs)
{
if (lhs.x < rhs.x or (lhs.x == rhs.x and lhs.y < rhs.y))
return std::strong_ordering::less;
if (lhs.x > rhs.x or (lhs.x == rhs.x and lhs.y > rhs.y))
return std::strong_ordering::greater;
return std::strong_ordering::equivalent;
}
};


int main() {
Point p1{0,0};
Point p2{0,-1};
Point p3{1,0};


std::cout << (p1 < p2) << " " << (p2 <= p3) << " " << (p3 > p1) << std::endl;
}
21🔥1
Перегрузка операторов (часть 2)

Ладно, вы еще не устали? Тогда вот вам на десерт самый абсурдный оператор. Оператор перечисления ,. Да, да, его можно перегрузить. Давайте для начала вспомним когда он может использоваться. Ну например в следующих строчках:

int main() {
int a = 0;
int b = 0;
for (a = 0, b = 0; a < 2 && b < 2; a++, b++);
return a, b;
}


Что тут делает этот оператор? Правильно, вычисляет по цепочке все выражения и возвращает самое правое. Давайте попробуем его перегрузить:

struct Crazy {
int a;


int operator,(Crazy &ob2)
{
return a;
}
};


int main() {
Crazy a{1};
Crazy b{2};


return a, b;
}


Результатом данной программы будет 1, то есть мы заставили вернуть левый объект вместо правого. Отлично, а нафига это нужно? Ну есть парочка интересных примеров (которые можно глянуть тут: https://stackoverflow.com/questions/5602112/when-to-overload-the-comma-operator). Вот один из них: Boost.assign умеет делать так:

vector<int> v;
v += 1,2,3,4,5,6,7,8,9;

Перегрузив запятую позволяет создавать цепочки на добавление элементов в коллекцию.

Ну что же, теперь вы знаете на 3 бессмысленных фичи С++ больше. Поздравляю вас, еще пару тысяч таких и мы с вами научимся проходить самые душные C++ интервью!
👍211🔥1
Оптимизация с использованием данных профилятора. Часть 1

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

Как вы знаете clang является оптимизирующим компилятором. То есть он пытается перепаковать исходный код в нечто наиболее эффективное для конкретной архитектуры процессора. И для большей части оптимизаций знания самого кода вполне достаточно. Однако существует ряд оптимизаций, которые могут работать эффективнее в случае если на этапе компиляции известно как код используется во время исполнения. К таким оптимизациям относятся к примеру: девиртуализация вызовов виртуальных методов (devitualization of virtual methods), порядок расположения блоков кода в исполняемом файле (instructions memory layout) и подстановка функций (functions inling). Для простоты статьи я остановлюсь подробнее на последней оптимизации, про первые две есть неплохое видео на ютуб, ссылку на которое я оставлю в конце статьи.

Для начала коротко про подстановку функций. Допустим у нас есть следующий код:

int foo(int arg) {
return arg + 1;
}


int main(int argc, char*argv[]) {
return foo(argc); // Место вызова функции
}


При прямолинейной компиляции этого кода мы получим что-то типа:

foo(int):
lea eax, [rdi + 1] // Логика функции
ret


main:
... // Инициализация аргументов
call foo(int) // Прыжок в область кода с реализацией функции
ret


Недостаток этого кода в том, что прыжок (call) является накладной операцией, так как переходы по сегментам исполняемого файла влияют на эффективность исполнение и компиляторы стараются сводить их к минимуму.

Если мы соберем этот пример с флагом -O2 мы увидим следующий код:
main:
lea eax, [rdi + 1]
ret

То есть компилятор подставил код самой функции вместо ее вызовы.

У вас скорее всего возник вопрос “а почему бы тогда не подставлять вообще все функции в программе?”. Хороший вопрос, однако большинство функций в реальном коде достаточно большие и вызываются во многих местах кода, если подставлять их всех везде размер исполняемого файла очень быстро станет слишком большим. Компиляторы стараются соблюдать некий компромисс между скоростью исполнения и размером бинарного файла. Для этого есть понятие квоты подстановок и при обычных условиях компилятор руководствуется рядом эвристик для того чтобы решить какой из методов подставить. Допустим в следующем примере:

int foo(int arg) {
return arg + 1;
}

int bar(int arg) {
int c = arg * 2;
int d = c + arg * 3;
return c + d;
}

int main(int argc, char*argv[]) {
int result;
for (int i = 0; i < 1000000000; i++) {
if (argc > 2) {
result += foo((int)argv[2][0]);
} else {
result += bar((int)argv[1][0]);
}
}


return result;
}


При прочих равных если у компилятора осталась одна квота на подстановку, то он выберет функцию foo так как она короче (я сейчас игнорирую остальные оптимизации).
Однако что если в реальной жизни пользователь намного чаще вызывает эту программу с одним аргументом? Проще говоря, если функция bar вызывается намного чаще, чем функция foo то ее подстановка становится более выгодной. Но что бы понять это компилятору нужна информация о том как программа используется пользователем.

Способов получения этой информации несколько, один из них – использование стороннего профилятора во время выполнения для сбора семплирования. В линуксе таким профилятором для CPU служит утилита perf.
🔥1👀1
Оптимизация с использованием данных профилятора. Часть 2

Давайте посмотрим как это может работать.

Шаг 1: Компилируем программу с несколькими флагами позволяющими сопоставить собранные метрики с исходным кодом:
clang++ -O2 -gline-tables-only -fdebug-info-for-profiling -funique-internal-linkage-names sample.cc -o sample

Шаг 2: Исполняем собранную программу с профилятором:
sudo perf record -b -e BR_INST_RETIRED.NEAR_TAKEN:uppp ./sample 1

Шаг 3: Так как perf это внешняя для llvm утилита, нам нужно сконвертировать результат ее работы в формат понятный для компилятора. Для этого в llvm есть утилита llvm-profgen:

llvm-profgen --binary=./sample --output=sample.prof --perfdata=perf.data

Шаг 4: Перекомпилируем исходную программу с собранной статистикой:
clang++ -O2 -fprofile-sample-use=sample.prof sample.cc -o sample_pgo

Давайте глянем на собранную статистику профилятора. Для этого в llvm есть другая утилита llvm-profdata которая позволяет объединять статистики, смотреть их содержимое и многое другое:

llvm-profdata show --sample sample.prof


Команда выше покажет нам следующее:
Function: main: 1965000, 0, 7 sampled lines
Samples collected in the function's body {

6: 82500, calls: _Z3bari:82500

}

Имея эту информацию на руках компилятор может понять что функция bar вызывается намного чаще чем функция foo и проработать эффективнее. Важное замечание: Если сценарий использования программы в реальной жизни резко меняется, то результат сбора PGO данных может повлиять негативно на производительность программы, так что чаще всего в реальной жизни PGO данные пере-собираются в реальном времени.

В следующей части я попробую собрать сам clang вместе и без PGO и сравню производительность сборки какого нибудь примера.

На последок видео о котором я говорил в начале: https://www.youtube.com/watch?v=3RtMMHkVsDg
🔥7