Современные процессоры устроены совсем не так, как в 80-х. Здесь, мне кажется, нужны замеры - насколько это медленнее. Нельзя же производительность в этом случае линейно, по количеству команд, мерить.
Отсутствие флагов это не убогость, а преимущество. Все современные архитектуры отказываются от флагов, т.к. это лишнее глобальное состояние, из-за которого в конвейере процессора даже не зависимые по данным команды начинают зависеть друг от друга, а это очень сильно мешает распараллеливанию выполнения.
Это будет в два раза медленнее, чем это возможно. Зачем так делать? Я с трудом представляю архитектуру, где не было бы adc, но даже, если это так, то перенос легко вычислить для полной суммы:
Он и так это без костылей оптимизирует: adc, sbb всякие, simd где может использует. О каких костылях речь ? На 32-х битной архитектуре 64-х битные целочисленные операции (long int) уже через вызовы отдельных функции выполняются, а как иначе, это костыли?
Потому-что на практике это не сложно сделать в виде сторонней библиотеки, а это принцип С++ - не тащить в компилятор то, что не нужно. Есть подход big integer - когда число не ограничено по длине и фактически его придется хранить в динамической памяти (привет Питону). В этом случае придется расплачиваться быстродействием. Есть подход long integer - когда необходимо работать с числами большими, чем поддерживаются процессором, но при этом фиксированной длины. Такие числа можно хранить на стеке и работать с ними почти также быстро как со встроенными, с поправкой на длину. Например, данный подход используется в Simple long integer math library for C++. Такие числа почти взаимозаменяемые со стандартными, не считая некоторых нюансов в автоматических преобразованиях.
Древнее оборудование? Метод близнецов - это же, по моему, buddy-allocator, который используется во всех операционных системах для выделения памяти. За счет скорости, простоты и неподверженности фрагментации, он идеально подходил для ядра ОС, где все страницы по замеру равны степени двойки. Он используется в ядре Linux, Windows, RTOS. Да и во многих современных Си-шных быстрых аллокатарах он используется как нижележащий слой, под аренами и прочими структурами. Так что, списывать его со счетов я бы не стал.
Странно, что ничего не сказано про выравнивание. х86, по умолчанию, позволяет доступ к не выровненным данным, тогда как многие другие платформы жестко требуют выравнивания.
Дополнения к std::shared_ptr и std::make_shared: 1. Без объяснения как на практике устроен std::shared_ptr, невозможно понять что за сценой присутствует control block и без std::make_shared у нас появиться дополнительный уровень косвенности и следственно лишнее выделение памяти под него. std::make_shared объединяет эти два выделения, делая создание и удалении быстрее. 2. std::weak_ptr естественно делает увеличение счетчика ссылок, но это отдельный счетчик, который также находится в control block-е.
На БК0010(01)/11/11M - на базе PDP-11 - не было целочисленных mul и div. op-коды были, но аппаратно команды были не реализованы. Даже сдвиг битовый был только одинарным: asl r0 - сдвинуть на один бит влево и вся роскошь. Чего уж тут говорить о Floating Point.
Под независимыми, я имел в виду «независимые» по коду, в конкретном модуле. Но их может связать сторонний по отношению к модулю класс, динамически. Многие просто об это забывают и из-за этого представляют себе возможности RTTI уж слишком примитивно.
Ну и что с того, что с наследует a и b? От этого связывание не становится статическим, а всё-равно происходит динамически в рантайме, при работе программы. Пофантазируйте: определения с у вас вообще может не быть, если подлинкована либа или подгружена dll с неизвестным вам классом.
(вопросы совместимости RTTI и целесообразности такого подхода на практике уберем за скобки)
Ну например, можно рассмотреть простейшую реализацию COM на dynamic_cast<>:
#include <iostream>
#include <memory>
#include <string>
// a.h include
struct a_base {
virtual std::string who() = 0;
virtual ~a_base() = default;
};
struct a : a_base {
std::string who() override { return "a class"; }
};
// b.h include
struct b_base {
virtual std::string who() = 0;
virtual ~b_base() = default;
};
struct b : b_base {
std::string who() override { return "b class"; }
};
// c.h include
struct c : a, b
{
};
// somewhere else
b_base* create_b()
{
return new c;
}
// main
int main()
{
b_base* b = create_b();
std::cout << b->who() << "\n";
a_base* a = dynamic_cast<a_base*>(b);
if (a != nullptr) {
std::cout << "receive a_base from pointer to b_base\n";
std::cout << a->who() << "\n";
} else {
std::cout << "can't receive a_base from pointer to b_base\n";
}
delete b;
b = nullptr;
return 0;
}
Обратите внимание, что иерархия класса a и класса b могут друг о друге ничего не знать, а также оба вообще могут не знать, что в программе есть класс, который наследует их оба. Однако это не мешает dynamic_cast отслеживать эту связь в рантайме и получать через b_base указатель на a_base.
В С++ всё это называет cross cast. Идея думаю понятна.
Динамик каст делает больше чем просто енамчик, там сохраняется информация о иерархии. Например с вашей реализацией вы не сможете привести к другой базе, то есть это не точно тот тип, но находящийся в той иерархии
На самом деле мало кто задумывается как работает dynamic_cast. С его помощью можно привести не просто к другому типу, который является наследником, а к типу вообще находящемуся во вне иерархии того или иного класса.
Всё верно. Как и многие конструкции в С++, if constexpr также не является «серебряной» пулей и не предназначена для замены механизма перегрузки функций. Следовательно её нужно использовать с головой, особенно при проектировании библиотек, которые будут использоваться сторонними программистами.
Действительно, если написать общий шаблон c диспетчеризацией, условно, что-то типа:
такой подход, создаст массу проблем с расширением, особенно если он используется во внешнем API.
С другой стороны, для того, что бы заменить кучу бойлерплейта, построенного на частичных перегрузках классов внутри реализации, это самое то:
template<uint_t sidx, typename variant_t>
inline void print_variant(const variant_t& data, size_t idx)
{
if constexpr (sidx < std::variant_size_v<variant_t>) {
if (idx == 0)
format(std::get<sidx>(data));
else
print_variant<sidx + 1>(data, idx - 1));
} else
throw v2::Exception("Variant index is out of range.");
}
— выводим значение std::variant по известному индексу в рантайме.
Полностью согласен, но хотел бы еще более конкретизировать:
1. Если требуется несколько сложное состояние, инвариант, в этом случае требуется объект, который и будет поддерживать это состояние через внешний интерфейс, в строгой согласованности.
2. Также объект может быть полезен, если некие данные абсолютно независимы друг от друга, могут меняться не согласованно, но по логике очень часто передаются вместе. Частный случай — структура.
Во всех остальных случаях функции предпочтительнее и гибче, на мой взгляд.
Более того, начиная с С++20 битовое представление signed integers фиксировано стандартом как дополнение до двух (two's complement).
However, all C++ compilers use two's complement representation, and as of C++20, it is the only representation allowed by the standard, with the guaranteed range from -2^(N-1) to +2^(N-1) — 1 (e.g. -128 to 127 for a signed 8-bit type).
8-bit ones' complement and sign-and-magnitude representations for char have been disallowed since C++11 (via CWG 1759), because a UTF-8 code unit of value 0x80 used in a UTF-8 string literal must be storable in a char element object.
Современные процессоры устроены совсем не так, как в 80-х. Здесь, мне кажется, нужны замеры - насколько это медленнее. Нельзя же производительность в этом случае линейно, по количеству команд, мерить.
Отсутствие флагов это не убогость, а преимущество. Все современные архитектуры отказываются от флагов, т.к. это лишнее глобальное состояние, из-за которого в конвейере процессора даже не зависимые по данным команды начинают зависеть друг от друга, а это очень сильно мешает распараллеливанию выполнения.
В любом случае, если в архитектуре нет команды adc, то будут накладные расходы, но не большие.
Вот например выхлоп для RISC-V:
Выглядит очень не плохо, на мой взгляд.
Это будет в два раза медленнее, чем это возможно. Зачем так делать? Я с трудом представляю архитектуру, где не было бы adc, но даже, если это так, то перенос легко вычислить для полной суммы:
Он и так это без костылей оптимизирует: adc, sbb всякие, simd где может использует. О каких костылях речь ? На 32-х битной архитектуре 64-х битные целочисленные операции (long int) уже через вызовы отдельных функции выполняются, а как иначе, это костыли?
Потому-что на практике это не сложно сделать в виде сторонней библиотеки, а это принцип С++ - не тащить в компилятор то, что не нужно.
Есть подход big integer - когда число не ограничено по длине и фактически его придется хранить в динамической памяти (привет Питону). В этом случае придется расплачиваться быстродействием.
Есть подход long integer - когда необходимо работать с числами большими, чем поддерживаются процессором, но при этом фиксированной длины. Такие числа можно хранить на стеке и работать с ними почти также быстро как со встроенными, с поправкой на длину. Например, данный подход используется в Simple long integer math library for C++. Такие числа почти взаимозаменяемые со стандартными, не считая некоторых нюансов в автоматических преобразованиях.
Древнее оборудование? Метод близнецов - это же, по моему, buddy-allocator, который используется во всех операционных системах для выделения памяти. За счет скорости, простоты и неподверженности фрагментации, он идеально подходил для ядра ОС, где все страницы по замеру равны степени двойки. Он используется в ядре Linux, Windows, RTOS. Да и во многих современных Си-шных быстрых аллокатарах он используется как нижележащий слой, под аренами и прочими структурами. Так что, списывать его со счетов я бы не стал.
Ладно порядок, даже размер слова и байта пропустили.
Странно, что ничего не сказано про выравнивание. х86, по умолчанию, позволяет доступ к не выровненным данным, тогда как многие другие платформы жестко требуют выравнивания.
Всё верно. Это обратное следствие. Как всегда дилемма: скорость-память.
Также ещё из минусов make_shared: custom deleter не задать, aliasing constructor не вызвать.
Дополнения к std::shared_ptr и std::make_shared:
1. Без объяснения как на практике устроен std::shared_ptr, невозможно понять что за сценой присутствует control block и без std::make_shared у нас появиться дополнительный уровень косвенности и следственно лишнее выделение памяти под него. std::make_shared объединяет эти два выделения, делая создание и удалении быстрее.
2. std::weak_ptr естественно делает увеличение счетчика ссылок, но это отдельный счетчик, который также находится в control block-е.
На БК0010(01)/11/11M - на базе PDP-11 - не было целочисленных mul и div. op-коды были, но аппаратно команды были не реализованы. Даже сдвиг битовый был только одинарным: asl r0 - сдвинуть на один бит влево и вся роскошь. Чего уж тут говорить о Floating Point.
(вопросы совместимости RTTI и целесообразности такого подхода на практике уберем за скобки)
В С++ всё это называет cross cast. Идея думаю понятна.
Действительно, если написать общий шаблон c диспетчеризацией, условно, что-то типа:
такой подход, создаст массу проблем с расширением, особенно если он используется во внешнем API.
С другой стороны, для того, что бы заменить кучу бойлерплейта, построенного на частичных перегрузках классов внутри реализации, это самое то: — выводим значение std::variant по известному индексу в рантайме.
1. Если требуется несколько сложное состояние, инвариант, в этом случае требуется объект, который и будет поддерживать это состояние через внешний интерфейс, в строгой согласованности.
2. Также объект может быть полезен, если некие данные абсолютно независимы друг от друга, могут меняться не согласованно, но по логике очень часто передаются вместе. Частный случай — структура.
Во всех остальных случаях функции предпочтительнее и гибче, на мой взгляд.