Грокаем C++
9.32K subscribers
49 photos
1 video
3 files
677 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​Выравнивание. А зачем?
#новичкам

Представим, что мы пишем что-то связанное с графикой. У нас есть плоскость черно-белых пикселей. Каждый пиксель определяется через цвет черно-белого тона и координаты на плоскости:

struct Pixel {
unsigned char grayscale;
int x, y;
};


Мы хотим сделать супер-мега-блезингово-быстрое приложение, которое при работе будет тратить всего 1 бит памяти. Ну или сколько получится, но как можно меньше. Поэтому мы паримся, как о перфомансе, так и о эффективности по памяти.

Если паримся, значит надо считать.

Посчитаем, сколько байт занимает структура Pixel. Это по сути sizeof(unsigned char) + 2 * sizeof(int). размер char'a 1 байт, размер int на современных десктопах 4 байта. С помощью применения дифференциального исчисления в пространстве Минковского получаем 9 байт.

Теория - это прекрасно, но надо все проверять:

std::cout << sizeof(Pixel) << std::endl;


И получаем..... 12 байт.

/+ 33% от расчетов. Вообще неоптимально получается. Но почему расчеты не сходятся с реальностью?

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

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

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

То есть высокая скорость обработки данных достигается в случае, когда адреса выровнены по нужной границе.

Теперь вернемся к Pixel. Представим, что мы создаем объект структуры:

Pixel p{255, 1, 1};


Исходя из рассуждений выше, логично предположить, что начало p будет лежать по уже выравненному адресу для более быстрого чтения. Допустим, он делится на 4. В начале структуры лежит 1 байт цвета. А после лежат 2 int'а по 4 байта, доступ к которым должен быть выровнен. Но если к числу, кратному 4-м, прибавить 1, то оно перестанет делиться на 4. То есть, если данные лежат прям последовательно, то доступ будет невыровненный, а значит медленный.

Но компилятор у нас - не тупая машина, питающаяся текстом программы. Он умный и умеет оптимизировать. Если для скорости работы программы нужно, что адреса x и y должны быть выровнены по четверке, то он сделает магию и выровняет их. Заклинание простое - добавляем 3 байта после поля grayscale и дело в шляпе! Вот поэтому на практике размер Pixel равен 12 байтам.

Ну ладно, мы еще повоюем за память. Попробуем перетасовать поля:

struct Pixel {
int x, y;
unsigned char grayscale;
};


Теперь-то будет 9 байт размер?!

Да нет. Все те же 12 байт.

Представьте, что вы сделали массив пикселей. Да, у первого элемента x и y выровнены по 4-ке. Но начиная со второго элемента выравнивание поедет к черту. Так не годится и приходят на помощь все те же 3 байта, только уже в конце структуры.

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

Сегодня мы рассмотрели, фактически, только предназначение выравнивания. В следующих постах будем раскрывать подробности.

Align yourself. Stay cool.

#cppcore #optimization #compiler
139👍19🔥7❤‍🔥3😁3
Выравнивание. Принципы
#новичкам

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

🔍 Однобайтовые данные не нужно выравнивать. Да, процессор читает данные определенными порциями, которые больше 1 байта. Но для любой взятой char переменной, при попытке ее прочитать ее значение будет лежат в самом начале порции. Минимальная адресуемая единица - это 1 байт, поэтому однобайтовые данные неделимы. Не нужно никаких дополнительных плясок.

Для наглядности можно представить себе массив чаров:

char array[10];
std::cout << sizeof(array) << std::endl;
// OUTPUT: 10


Это просто 10 подряд идущих символов. И адреса каждого символа не нужно выравнивать. Поэтому и количество байт, которое занимает этот массив, равно просто количеству его элементов. Без всяких дополнений.

🔍 Адрес поля тривиального типа должен быть выровнен по размеру этого типа

- для short адреса должны быть кратными 2
- для int адреса должны быть кратными 4
- для size_t адреса должны быть кратными 8
- для float адреса должны быть кратными 4
- для double адреса должны быть кратными 8
- для указателей адреса должны быть кратными 8

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

Все это актуально для современных 64-битных десктопов.

🔍 Адреса полей кастомных классов должны быть выровнены по длине самого жирного тривиального типа, входящего в состав класса. Не по размеру самого объекта, а по самому жирному его полю.

Например:

struct A {
char a;
char b;
};

struct B {
char first;
A second;
};

std::cout << sizeof(A) << std::endl;
std::cout << sizeof(B) << std::endl;
// OUTPUT:
// 2
// 3


У нас есть составная структура А, размер которой равен 2 байтам. Структура B содержит А в качестве поля. Так как в А только char поля, для доступа к ним не нужны паддинги. Поэтому размер В равен 3-м.

Но если будет вот так:

struct A {
char a;
double b;
};

struct B {
char first;
A second;
};

std::cout << sizeof(A) << std::endl;
std::cout << sizeof(B) << std::endl;
// OUTPUT:
// 16
// 24


Мало того, что паддинг размером 7 добавися перед полем b структуры А, так он же еще добавится перед `second` в В. И в B он такого размера именно потому, чтобы доступ до b в итоге всех размещений был выровнен.

🔍 Паддинг добавляется в основном перед тем полем, адрес которого должен быть выровнен. Мы это уже увидели на практике.

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

Допустим, у нас есть структура и мы хотим из нее сделать массив:

struct A {
double a;
char b;
};

A array[10];


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

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

🔍 Чтобы оптимально использовать память, лучше располагать поля в порядке убывания их требований к выравниванию. Так вы предыдущим полем гарантируете нужное выравнивание следующему. Сравните:

struct A {
char a;
// 7 byte padding
double d;
short b;
// 2 byte padding
int c;
};

struct B {
double d;
int c;
short b;
char a;
// 1 byte padding
};

std::cout << sizeof(A) << std::endl;
std::cout << sizeof(B) << std::endl;
// OUTPUT:
// 24
// 16


Реальный размер данных - 15 байт. Для double поля в любом случае нужно выравнивание по 8-ке. Но правильно расположив данные, мы экономим 8 байт.

Тема выравнивания довольно большая и мы прошлись только по верхам. Но даже эти простые принципы помогут вам правильно оценивать размер структур и организовывать их поля оптимальным образом.

Align yourself. Stay cool.

#cppcore #optimization #compiler
1🔥2716👍12
alignof
#опытным

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

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

С мебелью все просто - идешь и меряешь. Или смотришь размеры в онлайн магазине.

С данными так же, для них есть определенные требования к размеру и размещению. Но как их узнать?

Ну размер типа узнать просто - используем оператор sizeof, это все знают.

А требования к выравниваю как узнать?

Для этого есть C++11 оператор alignof. Он возвращает требуемое выравнивание для типа в байтах. Это значит, что адрес начала объекта данного типа был кратен результату alignof. Выравнивание должно быть степенью двойки(компьютеры у нас все-таки двоичные и все вокруг ее степени крутится).

В прошлом посте я голосновно перечислил выравнивания для базовых типов. Но теперь есть пруфы:

std::println("char требует выравнивания: {} байт", alignof(char));
std::println("short требует выравнивания: {} байт", alignof(short));
std::println("int требует выравнивания: {} байт", alignof(int));
std::println("size_t требует выравнивания: {} байт", alignof(size_t));
std::println("float требует выравнивания: {} байт", alignof(float));
std::println("double требует выравнивания: {} байт", alignof(double));
std::println("void* требует выравнивания: {} байт", alignof(void*));
// OUTPUT:
// char требует выравнивания: 1 байт
// short требует выравнивания: 2 байт
// int требует выравнивания: 4 байт
// size_t требует выравнивания: 8 байт
// float требует выравнивания: 4 байт
// double требует выравнивания: 8 байт
// void* требует выравнивания: 8 байт


Очень важно еще раз проговорить: размер типа и его выравнивание - это разные вещи. Так уж получилось, что они совпадают у тривиальных типов, но это не так для кастомных. Вот например:

std::println("std::string: размер={}, выравнивание={}", sizeof(std::string), alignof(std::string));
std::println("std::vector<int>: размер={}, выравнивание={}", sizeof(std::vector<int>), alignof(std::vector<int>));
// OUTPUT:
// std::string: размер=32, выравнивание=8
// std::vector<int>: размер=24, выравнивание=8


Кстати говоря. 8 - не максимальный размер выравнивания, некоторые типы требуют большего числа:

std::println("alignof(long double) = {}", alignof(long double));
std::println("alignof(std::max_align_t) = {}", alignof(std::max_align_t));
std::println("alignof(__m128) = {}", alignof(__m128));
// OUTPUT
// alignof(long double) = 16
// alignof(max_align_t) = 16
// alignof(__m128) = 16


double на самом деле не самую большую точность имеет из стандартных типов. Есть тип long double, его размер и выравнивание равны 16(на gcc и clang).

Также есть специальный тип std::max_align_t, чьи требования к выравниванию удовлетворяют любому скалярному типу. Тоже 16.

Для sse векторных регистров выравнивание тоже побольше.

Для чего этот оператор может применяться помимо учебных целей - рассмотрим в следующих постах.

Align yourself. Stay cool.

#cppcore #cpp11 #compiler
1👍3011🔥82
​​alignas
#опытным

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

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

В мире С++ можно контролировать почти все и alignment не исключение. Мы можем сами своими руками указать компилятору, как нужно выровнять конкретные тип.

Для этого существует C++11 спецификатор alignas. Он применяется к объявления типа или переменной и устанавливает новые требования для их выравнивания.

Сравним:

struct Vector4 {
float x, y, z, w;
};
struct alignas(16) Vector4Aligned {
float x, y, z, w;
};

std::cout << "Alignment of Vector4 is " << alignof(Vector4) << std::endl;
std::cout << "Alignment of Vector4Aligned is " << alignof(Vector4Aligned) << std::endl;

float data[8] = {1, 2, 3, 4, 5, 6, 7, 8};
alignas(32) float aligned_data[8] = {1, 2, 3, 4, 5, 6, 7, 8};

std::cout << "Alignment of data is " << alignof(data) << std::endl;
std::cout << "Alignment of aligned_data is " << alignof(aligned_data) << std::endl;
// OUTPUT:
// Alignment of Vector4 is 4
// Alignment of Vector4Aligned is 16
// Alignment of data is 4
// Alignment of aligned_data is 32


Без alignas данные выровнены по границе 4, как это нужно типу float. Используя же спецификатор, мы можем изменить требования.

Можно также выровнять данные на основе другого типа:

struct alignas(float) struct_float
{
// ...
};


Заметьте кстати, что в предыдущих примерах размер данных не изменялся, потому что мы выровняли по границе размера данных. Но можно же использовать и более сильные требования:

struct alignas(32) Vector4AlignedTooMuch {
float x, y, z, w;
};

std::cout << "Alignment of Vector4AlignedTooMuch is "
<< alignof(Vector4AlignedTooMuch) << ", and size is "
<< sizeof(Vector4AlignedTooMuch) << std::endl;
// OUTPUT:
// Alignment of Vector4AlignedTooMuch is 32, and size is 32


В этом случае размер данных увеличился в соответствие с усиленным выравниванием и добавлением паддингов.

Выравнивание переменных же аффектит только их адрес, потому что тип уже фиксирован.

Получается, что в определенных случаях мы тратим дополнительную память в угоду выравниванию. Зачем это может быть нужно?

Например, у вас есть массив мьютексов, которым пользуются разные потоки.

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

Что будет, если одно из ядер изменит хотя бы один мьютекс? Кэш линии других ядер инвалидируются и заново будут загружаться из ОП. А это очень долго. И так будет повторяться при каждом захвате и отпускании замка.

Такая ситуация называется false sharing. Вы ожидаете, что данные не связаны и независимы, а на самом деле операции над одними данными влияют на другие.

В этом случае поможет выравнивание данных по границе размера кэш линии:

struct AlignedMutex
{
alignas(64) std::mutex mutex;
};
std::array<AlignedMutex, 10> mutexes;


Тогда каждый мьютекс будет в своей кэш-линии и операции над одним из них не будут инвалидировать соседние кэш-линии.

Другой пример использования alignas - векторные инструкции sse и avx. Современные процессоры имеют специальные инструкции для обработки нескольких данных одновременно. Этими инструкциями управляются данные в векторных регистрах, их длины - это 128, 256 или 512 бит. Для более быстрой загрузки данные должны быть выровнены по ширине соответствующих векторных регистров:

alignas(32) float aligned_data[8] = {1.0f, 2.0f, 3.0f, 4.0f, 
5.0f, 6.0f, 7.0f, 8.0f}
__m256 avx_reg = _mm256_load_ps(aligned_data);


В общем, очень полезная штука, must have to know.

Have your separate space. Stay cool.

#cppcore #cpp11 #compiler
23🔥12👍8🎉2👏1
​​std::aligned_alloc
#опытным

alignas задает требования к выравниваю для типа или переменной. И компилятор, при размещении объектов на стеке слушает и повинуется этим правилам.

Но, например, malloc следует только своим внутренним правилам. Он выравнивает адреса, но только по границе alignof(std::max_align_t]). Это 16 байт на современных десктопах.

Что делать, если мне нужны более строгие требования к адресу? Например нужно выровнять выделенные на куче данные по границе 32, 64 или вообще по размеру страницы 4096 байт?

Для этого используется С++17 функция aligned_alloc:

void* aligned_alloc( std::size_t alignment, std::size_t size);


где alignment - требования к выравниванию, а size - размер данных для аллокации в байтах. size должен быть кратным alignment. Функция выделяет просто size байт и не конструирует никаких объектов. Подразумевается также возможность выделить массив значений размером size/alignment, каждое из которых выравнено по границе alignment.

Используется это в аллокаторах, если нужно учитывать выравнивание выделяемой памяти:

template <typename T>
T *allocate_aligned(size_t count) {
if (count == 0)
return nullptr;

const size_t alignment = alignof(T);
const size_t type_size = sizeof(T);
const size_t total_bytes = count * type_size;

char *raw_memory =
static_cast<char *>(std::aligned_alloc(alignment, total_bytes));
if (!raw_memory)
throw std::bad_alloc();

for (size_t constructed = 0; constructed < count; ++constructed) {
new (raw_memory + (constructed * type_size)) T();
}

return reinterpret_cast<T *>(raw_memory);
}


С аллокаторами тут полет фантазий может далеко увести, но суть такая: если нужна по-особенному выравненная динамическая память - используем std::aligned_alloc.

Align yourself. Stay cool.

#cppcore #cpp17 #compiler
21👍8🔥4🤪2
​​Где аллоцируются элементы std::array?
#новичкам

Вроде бы простой вопрос с собеседования, но он имеет 2 уровня погружения. В зависимости от того, как вы ответите, интервьюер будет по-разному воспринимать глубину ваших знаний.

1 уровень

Элементы std::array аллоцируются на стеке. std::array - это очень тонкая обертка над сишными массивами, в которых элементы располагаются именно на стеке.

Это очень легко проверить, достаточно сравнить размеры двух арреев с разным количеством элементов:

std::array<int, 10> arr{0};
std::array<int, 20> arr1{0};

std::cout << sizeof(arr) << " " << sizeof(arr1) << std::endl;
// OUTPUT:
// 40 80


Два массива на 10 и на 20 элементов, каждый элемент занимает по 4 байта. И все сходится: размер arr 10*4 байт, а второго - 20*4.

2 уровень

Вообще говоря, std::array - это обычный объект. Объекты можно размещать на стеке, а можно и на ......... правильно, куче.

Делается это очень просто:

void* operator new(std::size_t size) {
void* ptr = std::malloc(size);
if (!ptr) throw std::bad_alloc{};
std::cout << "Allocation has happend" << std::endl;
return ptr;
}

int main() {
auto * ptr = new std::array{0, 1, 2, 3, 4};
delete ptr;
}
// OUTPUT:
// Allocation has happend


Один new и на стеке уже лежит лишь указатель, а сами элементы массива хранятся на куче.

Прям вот в таком виде вряд ли кто-то работать с std::array.

Но если std::array является полем какого-то класса, объекты которого аллоцируются на куче, то и элементы массива тоже будут на куче располагаться.

Поэтому ответ на вопрос из заголовка поста - где хотим, там и располагаем. А если чуть менее пафосно, но более точно - там, где аллоцирутся сам объект std::array.

Reach advanced levels. Stay cool.

#cppcore #cpp11 #memory #interview
33🔥12👍8❤‍🔥2
​​using namespace std
#новичкам

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

Начнем с namespace

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

В С++ тоже есть похожий механизм. Он называется пространство имен или namespace. Можно сказать, что это область, где вы можете определять сущности, логически связанные друг с другом.

Работает это примерно так:

namespace MyLib {
void hello() { ... }
}

MyLib::hello();


Мы определяем пространство имен MyLib, в котором будут все функции, классы и переменные, которые относятся только к этой либе. И при использовании функции hello мы обязаны указать перед ее именем неймспейс MyLib, чтобы компилятор понимал, где ему искать эту функцию.

Заодно и все, кто читает код, теперь понимают, что мы использует функцию hello именно из пространства MyLib и ни откуда-нибудь еще. Потому что может случиться ситуация, когда функция с именем hello будет не только в этом пространстве:

void hello() { ... }

namespace MyLib {
void hello() { ... }
}

namespace OtherLib {
void hello() { ... }
}

MyLib::hello();


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

Теперь std.

Это пространство имен для сущностей, которые уже реализованы за вас в стандартной библиотеке.

Допустим, вы хотите что-то на консоль вывести. Вы подключаете заголовочник <iostream> и используете глобальный объект cout из пространства имен std:

#include <iostream>

int main() {
std::cout << "Hello, World!\n";
}


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

Теперь using.

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

my_library::my_cool_module::my_class obj;


И если вы знаете, что:

1️⃣ в данном конкретном небольшом месте кода вы используете много сущностей из какого-то пространства имен

2️⃣ и нет никакой неоднозначности, как в примере с функцией hello

вы можете использовать конструкцию using namespace:

void foo() {
using namespace my_library::my_cool_module;
my_cool_class obj;
my_other_cool_class = my_cool_function(obj);
}


С ее помощью вы говорите компилятору: "Внутри этого блока кода я могу использовать сущности из этого пространства имен без префикса. Ищи неизвестные тебе имена там."

Ну а конструкция using namespace std говорит о том, что мы можем использовать абсолютно все стандартные сущности без указания префикса.

Почему же так никто не делает, если это избавляет от головной боли писать постоянно std::?

👉🏿 Код пишется для того, чтобы его читали. При указании неймспейса читатель сразу понимает, откуда берется данная сущность. Это улучшает читаемость.

👉🏿 Обузинг using namespace особенно в глобальном скоупе(вне функции и другого пространства имен) чреват конфликтом имен:
void hello() {}

namespace MyLib {
void hello() {}
}

using namespace MyLib;

hello();


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

👉🏿 Положите using namespace std в свой хэдэр и теперь проблемы будут у всего кода, который его подключает.

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

Ease is not always the best choice. Stay cool.

#cppcore #badpractice
27👍11🔥3❤‍🔥2🗿1
​​sync_with_stdio
#опытным

std::cout, std::cerr и std::cin - стандартные объекты потоков для работы с вводом-выводом в С++.

Но из С++ программы мы также можем вызывать и сишное апи для работы с IO. Например scanfprintf. Они также могут читать из консоли и писать в нее.

Но как связаны С++ и С апи для работы с IO? Мешают ли они друг другу и скремблят результаты операций? Или все хитрее?

Все хитрее)

И разбирать все будем на примере потока вывода std::cout, для других все аналогично, просто так нагляднее будет.

В базовой конфигурации std::cout и запись в stdout с помощью С апи синхронизированы. То есть следующие вызовы полностью эквивалентны(c- символьная переменная):

std::fputc(stdout, c);
// and
std::cout.rdbuf()->sputc(c);


Запись символа в буфер С++ объекта имеет тот же эффект, что и запись символа в буфер С потока.

На практике это означает, что синхронизированные потоки C++ не буферизуются, и каждая операция ввода-вывода на потоке C++ немедленно применяется к буферу соответствующего потока C. Это позволяет свободно смешивать C++ и C I/O операции.

Обращу внимание. Мы до сих пор говорим только об одном символе. И не спроста: для записи конкретного символа гарантируется синхронизация и потокобезопасность(thread-safety). Но записи в stdout из разных тредов могут перемешивать символы этих записей.

То есть при вызове:

std::cout << "Hello, World!" << std::endl;


по сути имплементация посимвольно записывает приветствие миру в stdout в цикле, вызывая std::putc(stdout, c). Обычно такие функции(типа putc) реализованы с помощью внутренних механизмов синхронизации, обеспечивая потокобезопасность.

И это предательски медленно! Поэтому дефолтные операции ввода-вывода в С++ такие медленные.

Синхронизируют запись каждого символа только трусы! Но мы-то с вами не трусы.

Можно отвязать С++ потоки от С потоков и сделать их независимыми.
Тогда у С++ потоков появляется свой буфер, который работает оптимальнее, чем посимвольная запись. Это может дать сильный буст к производительности стандартных IO операций и по скорости они могут сравниться с сишными.

Чтобы отвязать потоки нужно самой первой строчкой main вызывать следующую функцию. Например так

int main()
{
std::ios::sync_with_stdio(false);
std::cout << "a\n";
std::printf("b\n");
std::cout << "c\n";
}


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

a
c
b


Видно, что разное апи работает независимо и непоследовательно.

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

Be fast. Stay cool.

#cppcore #optimization #goodoldc #compiler
124🔥12👍8🤔5
Конфликт в действии
#опытным

Спасибо, @Ivaneo, за любезно предоставленный примерчик.

Посмотрите на этот код:

#include <iostream>

int read;

int main()
{
std::ios_base::sync_with_stdio(false);
std::cin >> read;
}


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

А получите ошибку сегментации: прилетит сигнал SIGSEGV.

Почему так? Мы же ничего незаконного не делали! Просто пытаемся читать в переменную, заблаговременно отвязав С++ потоки от сишных.

На самом деле сделали кое-что незаконное. Назвали переменную в честь системного вызова. Давайте по порядку.

Когда у нас потоки синхронизированы, для операций с потоками используется стандартное сишное апи.

Когда мы отвязываем потоки, то С++ ввод-вывод начинает работать самостоятельно и независимо. Имеются полноценные буферы и сложная система их менеджмента. А для общения с операционной системой можно использовать непосредственно системные вызовы.

Посмотрим на примере gcc.

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

/* Read NBYTES into BUF from FD.  Return the number read or -1.  */
ssize_t
__libc_read (int fd, void *buf, size_t nbytes)
{
return SYSCALL_CANCEL (read, fd, buf, nbytes);
}

weak_alias (__libc_read, read)


Таким образом символ read, соответствующий функции чтения данных из файлового дискриптора, является слабым(weak).

А переменная read из нашего кода является сильным символом из сегмента неинициализированных данных(bss).

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

То есть при запуске программы и попытке вызвать функцию read, будет "вызываться" переменная. Отсюда и сегфолт.

Все системные функции - это С функции. У них нет никаких неймспейсов, чтобы предотвращать клаш имен. Поэтому всегда избегайте имен, которые могут конфликтовать со стандартными библиотечными функциями(например readopenclosewriteexit).

Avoid name clash. Stay cool.

#cppcore #goodoldc #OS
19🔥11👍7🤯4❤‍🔥1
​​Объявления функций vs объявление переменной или зачем нужен extern
#новичкам

В С/С++ существует интересная механика - разделение на объявление сущности и ее определение. У этого есть серьезные причины:

👉🏿 Ускорение компиляции: изменения в реализации не требуют перекомпиляции всех файлов, использующих объявление.

👉🏿 Сокрытие реализации: инкапсуляция, предоставление только интерфейса (в .hpp) и скрытие деталей в .cpp.

👉🏿 Разрешение циклических зависимостей и возможность ссылаться на типы до их полного определения.

👉🏿 Организация кода и разделение ответственности.

И если с функциями более менее понятно. Есть объявление в виде обозначения сигнатуры функции. И определение в виде полного предоставления тела функции:

void foo(int a); // declaration

void foo(int a) {
std::cout << a << std::endl; // definition
}


С переменными все немного сложнее. Но начнем с легкого:

int i = 0;


Это определение хоть глобальной, хоть локальной переменной i и ее инициализация нулем. Все все понимают.

Если думать в аналогии с функциями, то легко дойти до мысли что:

int i;


это объявление переменной.

Но это не так!

Это в любом случае определение!

Если вы встретите такую строчку вне функции, то это определение переменной и ее инициализация нулем. А внутри функции - определение без инициализации(значение будет мусорное). Определение переменной всегда связано с созданием объекта и выделением памяти под него.

Тогда как сказать компилятору, что я буду использовать переменную с каким-то именем и типом, но не хочу создавать объект а буду сслаться на определение в другом месте?

Ровно для этого и нужно ключевое слово extern.

extern int i; // declaration

int main() {
std::cout << i << std::endl;
}

int i = 42;

// OUTPUT
// 42


До мейна мы сказали, что будем ссылаться на переменную i, определение которой лежит в другом месте. И сразу после мейна предоставляем это определение. Компилятор все понял и в итоге мы получаем 42 на консоли, а не ноль.

В этом случае строчка extern int i; является объявлением глобальной переменной i.

Определение переменной может находиться хоть в другой единице трансляции. Просто тогда зависимости будут разрешаться на этапе линковки.

Для локальных переменных функций объявление не предусмотрено(оно и не нужно). Если попытаетесь сделать так:

int main() {
extern int i;
std::cout << i << std::endl;
}


То вы опять объявите глобальную переменную i.

То есть чисто объявить вы можете только глобальную переменную (про полям класса речь не идет) и только с помощью extern.

Declare your intentions. Stay cool.

#cppcore
1🔥21👍118🤣3