Приятно видеть, что C++ настолько тщательно следит за обратной совместимостью.
Мы столько боли ради этой обратной совместимости пережили.
И теперь вдвойне приятно видеть, что поменяться может семантика вообще всего что угодно.
Например, вот это:
Оно нормально скомпилируется в С++20, но упадет на static_assert в C++23. Тоесть не просто перестанет компилироваться, а тихой сапой скомпилируется "в другое".
Насколько понимаю, связано вот с этим:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2266r3.html
Но меня уже разоблачили, плюсовик я не настоящий и гениальности очередного замысла могу не понять. Как хорошо, что у меня канал не образовательный а развлекательный 🙂
Мы столько боли ради этой обратной совместимости пережили.
И теперь вдвойне приятно видеть, что поменяться может семантика вообще всего что угодно.
Например, вот это:
decltype(auto) f(int&& x)
{
return (x);
}
static_assert(std::same_as<decltype(f(1)), int&>);
Оно нормально скомпилируется в С++20, но упадет на static_assert в C++23. Тоесть не просто перестанет компилироваться, а тихой сапой скомпилируется "в другое".
Насколько понимаю, связано вот с этим:
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2266r3.html
Но меня уже разоблачили, плюсовик я не настоящий и гениальности очередного замысла могу не понять. Как хорошо, что у меня канал не образовательный а развлекательный 🙂
godbolt.org
Compiler Explorer - C++
decltype(auto) f(int&& x)
{
return (x);
}
static_assert(std::same_as<decltype(f(1)), int&>);
int main(){}
{
return (x);
}
static_assert(std::same_as<decltype(f(1)), int&>);
int main(){}
😁21❤5🥴2🔥1🗿1
Те, кто со мной регулярно общаются, знают, что я ненавижу сраный move.
И всю move семантику. И у меня даже готовился доклад про то, как я его ненавижу, с кучей веселых примеров, которые не приехали еще сюда в дурку. Где я собрал в кучку почему именно я его ненавижу, но там получилось очень много и очень неструктурированно, поэтому доклад до сих пор в разработке.
Но вот один пример, который я выкину таки сюда.
Здесь классика классика.
Мапа из чего-то в в unique_ptr. Ну кто так не делал (здесь должна быть ссылке на open-source пример, но у меня жопа горит).
Дальше мы делаем emplace.
Ну и вот второй emplace с ключом 1 вернет false.
А что же произойдет с p?
Он занулится, потому что мы его мувнули в функцию.
Выводит
Тоесть в мапу значение не поместили, а из указателя оно пропало... Восхитительно.
Притом что если делать std::move "на месте" такого не происходит.
Да, это чинится тем, что мы убираем
Я задолбался ловить такие приколы с move.
И всю move семантику. И у меня даже готовился доклад про то, как я его ненавижу, с кучей веселых примеров, которые не приехали еще сюда в дурку. Где я собрал в кучку почему именно я его ненавижу, но там получилось очень много и очень неструктурированно, поэтому доклад до сих пор в разработке.
Но вот один пример, который я выкину таки сюда.
int main() {
std::map<int, std::unique_ptr<int>> m;
m.emplace(1, std::make_unique<int>(42));
auto p = std::make_unique<int>(100);
auto [it, inserted] = m.emplace(std::make_pair(1, std::move(p)));
}
Здесь классика классика.
Мапа из чего-то в в unique_ptr. Ну кто так не делал (здесь должна быть ссылке на open-source пример, но у меня жопа горит).
Дальше мы делаем emplace.
Return value
A pair consisting of an iterator to the inserted element (or to the element that prevented the insertion) and a bool value set to true if and only if the insertion took place.
Ну и вот второй emplace с ключом 1 вернет false.
А что же произойдет с p?
Он занулится, потому что мы его мувнули в функцию.
int main() {
std::map<int, std::unique_ptr<int>> m;
m.emplace(1, std::make_unique<int>(42));
auto p = std::make_unique<int>(100);
auto [it, inserted] = m.emplace(std::make_pair(1, std::move(p)));
std::cout << std::boolalpha;
std::cout << "inserted = " << inserted << '\n';
std::cout << "p is null = " << (p == nullptr) << '\n';
std::cout << "*it->second = " << *it->second << '\n';
}
Выводит
inserted = false
p is null = true
*it->second = 42
Тоесть в мапу значение не поместили, а из указателя оно пропало... Восхитительно.
Притом что если делать std::move "на месте" такого не происходит.
int main() {
auto p = std::make_unique<int>(100);
std::move(p);
std::cout << std::boolalpha;
std::cout << "p is null = " << (p.get() == nullptr) << '\n';
}
p is null = false
Да, это чинится тем, что мы убираем
make_pair, но это в тестовом примере, где это легко заметить. И когда уже знаешь проблему.Я задолбался ловить такие приколы с move.
godbolt.org
Compiler Explorer - C++
int main() {
std::map<int, std::unique_ptr<int>> m;
m.emplace(1, std::make_unique<int>(42));
auto p = std::make_unique<int>(100);
auto [it, inserted] = m.emplace(std::make_pair(1, std::move(p)));
std::cout << std::boolalpha;
std::cout…
std::map<int, std::unique_ptr<int>> m;
m.emplace(1, std::make_unique<int>(42));
auto p = std::make_unique<int>(100);
auto [it, inserted] = m.emplace(std::make_pair(1, std::move(p)));
std::cout << std::boolalpha;
std::cout…
🤷♂20😁11🤡3👎2😭2🔥1
Убедительная просьба - обновляйте компиляторы.
Помимо внедрения множества способов выстрелить себе в ногу, новые версии компиляторов чинят разные веселые баги.
Вот к примеру вот этот код:
Возвращаемая лямбда возвращаемая из метода
вот тут clang18.1 нормально сработает и модифицирует константный класс. clang19.1 выдаст ошибку компиляции. А gcc15.2 упадет с segfault
https://godbolt.org/z/8qqbehqhE
Помимо внедрения множества способов выстрелить себе в ногу, новые версии компиляторов чинят разные веселые баги.
Вот к примеру вот этот код:
struct S {
int x;
auto foo() {
return [*this](this auto&& self) {
__builtin_printf("%d ", x);
x = 10;
};
}
};
Возвращаемая лямбда возвращаемая из метода
foo капчурит this, и потом модифицирует значение. Но что будет, если мы сделаем лямбду константной?
int main() {
S s{ 5 };
const auto l = s.foo();
l();
s.x = 15;
l();
__builtin_printf("%d\n", s.x);
}
вот тут clang18.1 нормально сработает и модифицирует константный класс. clang19.1 выдаст ошибку компиляции. А gcc15.2 упадет с segfault
https://godbolt.org/z/8qqbehqhE
godbolt.org
Compiler Explorer - C++
struct S {
int x;
auto foo() {
return [*this](this auto&& self) {
__builtin_printf("%d ", x);
x = 10;
};
}
};
int main() {
S s{ 5 };
const auto l = s.foo();
l();
s.x = 15;
l();
__builtin_printf("%d\n", s.x);
}
int x;
auto foo() {
return [*this](this auto&& self) {
__builtin_printf("%d ", x);
x = 10;
};
}
};
int main() {
S s{ 5 };
const auto l = s.foo();
l();
s.x = 15;
l();
__builtin_printf("%d\n", s.x);
}
💊21🤔5
Ладно, разумеется прошлый пост был набросом.
Никогда не обновляйте компиляторы без очень серьезных на то причин. Мало ли что там сломается, а у вас не будет команды все это чинить.
Вот к примеру.
Вложенные лямбды с параметр паком.
В clang 17 они нормально компилируются.
В clang 20 они крашат компилятор.
В clang 22 это починили и они компилируются.
В текущем транке снова ломают компилятор.
Не выходи из комнаты, не совершай ошибку. Компилятор обновляют слабаки и трусы.
Никогда не обновляйте компиляторы без очень серьезных на то причин. Мало ли что там сломается, а у вас не будет команды все это чинить.
Вот к примеру.
Вложенные лямбды с параметр паком.
#include <utility>
template <std::size_t N, std::size_t M>
concept C = requires {
[]<std::size_t... Is>(std::index_sequence<Is...>)
requires requires {
[]<std::size_t... Js>(std::index_sequence<Js...>)
requires requires { true; }
{}(std::make_index_sequence<M>{});
}
{}(std::make_index_sequence<N>{});
};
static_assert(C<2, 3>);
В clang 17 они нормально компилируются.
В clang 20 они крашат компилятор.
В clang 22 это починили и они компилируются.
В текущем транке снова ломают компилятор.
Не выходи из комнаты, не совершай ошибку. Компилятор обновляют слабаки и трусы.
godbolt.org
Compiler Explorer - C++
template <std::size_t N, std::size_t M>
concept C = requires {
[]<std::size_t... Is>(std::index_sequence<Is...>)
requires requires {
[]<std::size_t... Js>(std::index_sequence<Js...>)
requires requires { true; }
{}(std::make_index_sequence<M>{});…
concept C = requires {
[]<std::size_t... Is>(std::index_sequence<Is...>)
requires requires {
[]<std::size_t... Js>(std::index_sequence<Js...>)
requires requires { true; }
{}(std::make_index_sequence<M>{});…
🤡15🦄10😁3🤔2❤1🔥1💩1👾1
Ладно, искать баги в компиляторах весело, но недостаточно.
Давайте поиграемся в чуть более интересные игры.
Вспомним, что у нас в С++ есть правило: структура не может иметь нулевой размер. Потому что у ее инстанса должен быть адрес, и здесь С++ как-то забыл про Zero Cost и все такое.
Зато С++ вспомнил про Zero Cost, когда мы примешиваем пустую структуру в наследование другой.
Вот эта штука скомпилируется. Тоесть у нас структуры без размера имеют размер, но этот размер не добавляется к размеру общей структуры.
Всвязи с чем я упоролся, и написал вот такой класс:
Свиду ничем не примечательный класс, который просто заполняет себя нулями. И это нормально работает для непустого класса, но пустой класс такими выкрутасами ломает соседние поля в классе, который его отнаследовал.
Выводит
(да, да, это UB, я знаю, прекратите. Мы тут поржать собрались или где?)
Но где-то тут в безумии зараждается идея... У нас же есть
А на gcc он кладет данные в padding других структур.
И тут я очень хотел развернуться мыслью, но быстро нашел уже закрытый баг с std::expected. (половина примеров выше взята из обсуждения этого бага, потому что они нагляднее чем то, с чем работал я).
Если коротко, то методы expected могли зачищать переменные вокруг себя, если те оказывались в из padding. Потому что emplace пересоздавал внутренний объект целиком.
Но, увы, примера воспроизводящего это дело у меня нет. А после этого примера все остальное рядом меркнет. Увы.
Давайте поиграемся в чуть более интересные игры.
Вспомним, что у нас в С++ есть правило: структура не может иметь нулевой размер. Потому что у ее инстанса должен быть адрес, и здесь С++ как-то забыл про Zero Cost и все такое.
Зато С++ вспомнил про Zero Cost, когда мы примешиваем пустую структуру в наследование другой.
struct Empty1 {};
struct Empty2 {};
struct NonEmpty {
int Value;
};
struct MyClass1
:public NonEmpty
,public Empty1
,public Empty2
{
};
struct MyClass2
:public Empty1
,public NonEmpty
,public Empty2
{
};
struct MyClass3
:public Empty1
,public Empty2
,public NonEmpty
{
};
static_assert(sizeof(NonEmpty) + sizeof(Empty1) + sizeof(Empty2) == 6);
static_assert(sizeof(int) == 4);
static_assert(sizeof(MyClass1) == 4);
static_assert(sizeof(MyClass2) == 4);
static_assert(sizeof(MyClass3) == 4);
int main() {}Вот эта штука скомпилируется. Тоесть у нас структуры без размера имеют размер, но этот размер не добавляется к размеру общей структуры.
Всвязи с чем я упоролся, и написал вот такой класс:
struct Empty {
void zero() { std::memset(this, 0, sizeof(*this)); }
};Свиду ничем не примечательный класс, который просто заполняет себя нулями. И это нормально работает для непустого класса, но пустой класс такими выкрутасами ломает соседние поля в классе, который его отнаследовал.
struct CompressedPair : Empty {
CompressedPair(Empty e, bool x) : Empty(e), b(x) { }
bool b;
};
int main() {
auto a = CompressedPair({}, true);
__builtin_printf("%b\n", a.b);
a.zero();
__builtin_printf("%b\n", a.b);
return 0;
}Выводит
1
0
(да, да, это UB, я знаю, прекратите. Мы тут поржать собрались или где?)
Но где-то тут в безумии зараждается идея... У нас же есть
no_unique_address.А на gcc он кладет данные в padding других структур.
#include <iostream>
template <typename T>
struct optional {
union {
char foo;
T val;
};
[[no_unique_address]] bool engaged;
};
template <typename View>
struct reverse_view {
[[no_unique_address]] optional<typename View::iterator> cache;
View base_;
};
struct MyView {
using iterator = int*;
char begin_;
};
int main() {
std::cout << "sizeof = " << sizeof(reverse_view<MyView>) << std::endl; // clang - 24, gcc - 16
std::cout << "alignof = " << alignof(reverse_view<MyView>) << std::endl;
}
И тут я очень хотел развернуться мыслью, но быстро нашел уже закрытый баг с std::expected. (половина примеров выше взята из обсуждения этого бага, потому что они нагляднее чем то, с чем работал я).
Если коротко, то методы expected могли зачищать переменные вокруг себя, если те оказывались в из padding. Потому что emplace пересоздавал внутренний объект целиком.
Но, увы, примера воспроизводящего это дело у меня нет. А после этого примера все остальное рядом меркнет. Увы.
godbolt.org
Compiler Explorer - C++
struct Empty1 {};
struct Empty2 {};
struct NonEmpty {
int Value;
};
struct MyClass1
:public NonEmpty
,public Empty1
,public Empty2
{
};
struct MyClass2
:public Empty1
,public NonEmpty
,public Empty2
{
};
struct MyClass3
…
struct Empty2 {};
struct NonEmpty {
int Value;
};
struct MyClass1
:public NonEmpty
,public Empty1
,public Empty2
{
};
struct MyClass2
:public Empty1
,public NonEmpty
,public Empty2
{
};
struct MyClass3
…
❤15🔥2💊1
#толькосвоимемы
Код взят тут.
Вообще по библиотеке много кода, который заставляет задумываться о вечном.
Код взят тут.
if (dim == 0) idx_dim = i0;
else if (dim == 1) idx_dim = i1;
else if (dim == 2) idx_dim = i2;
else if (dim == 3) idx_dim = i3;
const struct htp_tensor * src = (idx_dim < src0->ne[dim]) ? src0 : src1;
uint32_t s0 = i0;
uint32_t s1 = i1;
uint32_t s2 = i2;
uint32_t s3 = i3;
if (dim == 0 && src == src1) s0 -= src0->ne[0];
if (dim == 1 && src == src1) s1 -= src0->ne[1];
if (dim == 2 && src == src1) s2 -= src0->ne[2];
if (dim == 3 && src == src1) s3 -= src0->ne[3];
Вообще по библиотеке много кода, который заставляет задумываться о вечном.
🔥14
Если кто не знаком с библиотекой nlohmann/json - она прекрасна. Мои мысли о том, как должна выглядеть библиотека для пользователя, она начала формировать еще году в 2012. (я гне нашел подтверждений, что она тогда уже была, но по ощущениям где-то тогда и появилась)
Но речь не об этом.
Как написано в cppreference, если ключи равны, то и хеши от них равны.
```
Given the following values:
All following requirements are satisfied:
- If
- Unless
```
Иногда срабатывает даже если это объекты разных типов
А вот если вы упакуете те же типы в обертку типа json:
Тут уже как повезет. Конкретно для этой библиотеке не сраслось:
Потому что там есть своя специализация. И с ней это правило для hash не работает.
А потому эту штуку очень весело бывает использовать в hash-мапах, когда два одинаковых объекта могут стать разными ключами.
Сохраняем бдительность!!!!
Но речь не об этом.
Как написано в cppreference, если ключи равны, то и хеши от них равны.
```
Given the following values:
h, an object of type std::hash<Key>.k1 and k2, objects of type Key.All following requirements are satisfied:
- If
k1 == k2 is true, h(k1) == h(k2) is also true.- Unless
std::hash<Key> is a program-defined specialization, h(k1) will never throw an exception.```
Иногда срабатывает даже если это объекты разных типов
const auto x1 = 42;
const auto x2 = 42u;
__builtin_printf("%b\n\n", x1 == x2);
__builtin_printf("%#08zx\n%#08zx\n",
std::hash<int>{}(x1),
std::hash<unsigned>{}(x2));
1
0x00002a
0x00002a
А вот если вы упакуете те же типы в обертку типа json:
const auto j1 = json{x1};
const auto j2 = json{x2};
__builtin_printf("%b\n\n", j1 == j2);
__builtin_printf("%#08zx\n%#08zx\n",
std::hash<json>{}(j1),
std::hash<json>{}(j2));
Тут уже как повезет. Конкретно для этой библиотеке не сраслось:
1
0x286fec1b50
0x286fec1b91
Потому что там есть своя специализация. И с ней это правило для hash не работает.
namespace std // NOLINT(cert-dcl58-cpp)
{
/// @brief hash value for JSON objects
/// @sa https://json.nlohmann.me/api/basic_json/std_hash/
NLOHMANN_BASIC_JSON_TPL_DECLARATION
struct hash<nlohmann::NLOHMANN_BASIC_JSON_TPL> // NOLINT(cert-dcl58-cpp)
{
std::size_t operator()(const nlohmann::NLOHMANN_BASIC_JSON_TPL& j) const
{
return nlohmann::detail::hash(j);
}
};
А потому эту штуку очень весело бывает использовать в hash-мапах, когда два одинаковых объекта могут стать разными ключами.
Сохраняем бдительность!!!!
GitHub
nlohmann - Overview
You may know me from my JSON library for C++. nlohmann has 47 repositories available. Follow their code on GitHub.
🫡19🥴2❤1👍1
Всяко разное болезненное есть в С++, из всего болезненного одно из самых болезненных - это friend функции. Если жизнь и рассудок дороги вам - не объявляйте такие функции вообще никогда.
С ними связаны много разных интересных, скажем так особенностей, про которые эксперты из комментариев скажут, что все логично, так и надо, и вообще, а как вы хотели иначе?
Никогда такого не было и вот опять - нашел следующую веселую приколюху.
Пусть у нас есть вот такой класс:
Да, странненький и убогенький, но вообще весь мой код примерно такой.
Так вот в нем есть класс, у него есть вложенный класс, и в нем есть функция, которая модифицирует объекты и своего класса, и того, в который он вложен.
И как мы можем видеть из ссылки выше - все прекрасно работает, и проблем ни у кого не вызывает.
Но давайте теперь сделаем эту функцию
Милорд, у вас родился кудяблик, и эта штука перестала компилироваться:
Потому что что? Потому что правильно, мы дружим только с B, а вот с A не дружим.
Мне тут поступал запрос на глубину моих постов, поэтому я потратил целых 10 минут на изучение вопроса прежде чем пошел строчить гневный пост в интернеты, и нашел вот такое:
https://cplusplus.github.io/CWG/issues/3193.html
тут что-то про
Кароче, friend - это одна их тех вещей, которых я стараюсь избегать в своем коде... Рано или поздно я начну избегать всего С++
С ними связаны много разных интересных, скажем так особенностей, про которые эксперты из комментариев скажут, что все логично, так и надо, и вообще, а как вы хотели иначе?
Никогда такого не было и вот опять - нашел следующую веселую приколюху.
Пусть у нас есть вот такой класс:
class A {
int x;
public:
A(int a) : x(a) {}
class B {
int y;
public:
B(int b) : y(b) {}
void f(A& a, B& b){
b.y = 1;
a.x = 1;
}
};
};
Да, странненький и убогенький, но вообще весь мой код примерно такой.
Так вот в нем есть класс, у него есть вложенный класс, и в нем есть функция, которая модифицирует объекты и своего класса, и того, в который он вложен.
void f(A& a, B& b){
b.y = 1;
a.x = 1;
}
И как мы можем видеть из ссылки выше - все прекрасно работает, и проблем ни у кого не вызывает.
int main() {
auto a = A(1);
auto b = A::B(2);
b.f(a, b);
return 0;
}
Но давайте теперь сделаем эту функцию
friend и вынесем за пределы класса.
class A {
int x;
public:
A(int a) : x(a) {}
class B {
int y;
public:
B(int b) : y(b) {}
friend void f(A& a, B& b);
};
};
void f(A& a, A::B& b){
b.y = 1;
a.x = 1;
}
Милорд, у вас родился кудяблик, и эта штука перестала компилироваться:
int main() {
auto a = A(1);
auto b = A::B(2);
f(a, b);
return 0;
}
Потому что что? Потому что правильно, мы дружим только с B, а вот с A не дружим.
<source>:15:7: error: 'x' is a private member of 'A'
15 | a.x = 1;
| ^
<source>:2:9: note: implicitly declared private here
2 | int x;
|
Мне тут поступал запрос на глубину моих постов, поэтому я потратил целых 10 минут на изучение вопроса прежде чем пошел строчить гневный пост в интернеты, и нашел вот такое:
https://cplusplus.github.io/CWG/issues/3193.html
тут что-то про
inhabiting the class scope, и насколько я понял, вот такая ошибка компиляции - это самовольство компиляторов, а в стандарте про это вроде как ничего внятного сказано не было... Или было, но я, в скудоумии своем, не разобрался... Кароче, friend - это одна их тех вещей, которых я стараюсь избегать в своем коде... Рано или поздно я начну избегать всего С++
godbolt.org
Compiler Explorer - C++
class A {
int x;
public:
A(int a) : x(a) {}
class B {
int y;
public:
B(int b) : y(b) {}
void f(A& a, B& b){
b.y = 1;
a.x = 1;
}
};
};
int main() {
auto a = A(1);
…
int x;
public:
A(int a) : x(a) {}
class B {
int y;
public:
B(int b) : y(b) {}
void f(A& a, B& b){
b.y = 1;
a.x = 1;
}
};
};
int main() {
auto a = A(1);
…
❤17😁10👍2
Мошенники заставили пенсионерку из Москвы переписать квартиру на Rust
😁88🤡6❤1💯1
Я в очередной раз искал медь, а нашел золото.
Я чуть-чуть погрузился в bit_cast. Ввели такой в С++20 каст побитовый.
Он передает бит к биту из одного объекта в другой.
Условия просты:
```
sizeof(To) == sizeof(From) is true;
is_trivially_copyable_v<To> is true; and
is_trivially_copyable_v<From> is true.
```
И я искал воспроизводящий пример как он ведет себя с padding byte и ломается ли у нас что-то (если у вас такой есть - несите в предложку).
Но наткнулся на вот такой веселый пример.
И у нас clang весело выводит
x и равен нулю, и неравен нулю (оба сравнения false).
И если вы думаете, что на этом все закончено, то нет. Если убрать флаг -O2 из флагов компиляции, вывод будет
А в gcc вывод с O2 и без O2 выглядят так
Кароче, bit_cast временно вычеркиваю из инструментов до дальнейшего разбирательства.
Я чуть-чуть погрузился в bit_cast. Ввели такой в С++20 каст побитовый.
Он передает бит к биту из одного объекта в другой.
Условия просты:
```
sizeof(To) == sizeof(From) is true;
is_trivially_copyable_v<To> is true; and
is_trivially_copyable_v<From> is true.
```
И я искал воспроизводящий пример как он ведет себя с padding byte и ломается ли у нас что-то (если у вас такой есть - несите в предложку).
Но наткнулся на вот такой веселый пример.
struct alignas(int) Empty {};
static_assert(sizeof(Empty) == sizeof(int));
int main() {
const int x = std::bit_cast<int>(Empty{});
std::printf("%d %d", x == 0, x != 0);
}И у нас clang весело выводит
0 0
x и равен нулю, и неравен нулю (оба сравнения false).
И если вы думаете, что на этом все закончено, то нет. Если убрать флаг -O2 из флагов компиляции, вывод будет
0 1
А в gcc вывод с O2 и без O2 выглядят так
1 0
0 1
Кароче, bit_cast временно вычеркиваю из инструментов до дальнейшего разбирательства.
😁35🤯13🔥2❤1👍1🤔1
Ковырял тут вот эту библиотеку:
https://github.com/yandex/yaff/blob/c815bc0a2469201542c4734257628a98ed794c30/include/yaff/base.h#L146-L160
Это высокопроизводительный яндексовый сериализатор бинарного формата. Который берет протобафную схему, но конфигурирует совершенно другую сериализацию.
Нашел там вот такой код:
И пытался понять, зачем он там.
Что выяснилось. Мы хотим в сообщении "дефолтное" значение записывать нулевыми байтами. При этом при чтении хотим получать из нулевых байтов дефолтное значение. Поэтому мы при чтении делаем вот так:
А при записи вот так:
Из приятного: если наше дефолтное значение - нули, то компилятор этот xor соптимизирует.
Это ли не блеск, это ли не шик, это ли не прелесть.
https://github.com/yandex/yaff/blob/c815bc0a2469201542c4734257628a98ed794c30/include/yaff/base.h#L146-L160
Это высокопроизводительный яндексовый сериализатор бинарного формата. Который берет протобафную схему, но конфигурирует совершенно другую сериализацию.
Нашел там вот такой код:
template <typename T>
inline constexpr T XorDef(T v, T d) noexcept {
return v ^ d;
}
template <>
inline constexpr float XorDef(float v, float d) noexcept {
return std::bit_cast<float>(std::bit_cast<uint32_t>(v) ^ std::bit_cast<uint32_t>(d));
}
template <>
inline constexpr double XorDef(double v, double d) noexcept {
return std::bit_cast<double>(std::bit_cast<uint64_t>(v) ^ std::bit_cast<uint64_t>(d));
}
И пытался понять, зачем он там.
Что выяснилось. Мы хотим в сообщении "дефолтное" значение записывать нулевыми байтами. При этом при чтении хотим получать из нулевых байтов дефолтное значение. Поэтому мы при чтении делаем вот так:
YAFF_PURE T ReadValueUnsafe(const FieldOffset offset, const T defaultVal) const noexcept {
return XorDef(yaff::ReadValue<T>(Message() + offset), defaultVal);
}
YAFF_PURE T ReadValueUnsafe(const FieldOffset offset, const T defaultVal) const noexcept {
return XorDef(yaff::ReadValue<T>(Fields() + offset), defaultVal);
}
А при записи вот так:
void AddField(FieldId id, T value, T def) {
WriteField<T>(id, XorDef(value, def));
}
Из приятного: если наше дефолтное значение - нули, то компилятор этот xor соптимизирует.
Это ли не блеск, это ли не шик, это ли не прелесть.
GitHub
yaff/include/yaff/base.h at c815bc0a2469201542c4734257628a98ed794c30 · yandex/yaff
YaFF is a high-performance C++ serialization library that provides a zero-copy wire format for the Protobuf ecosystem. - yandex/yaff
❤16🥴3🔥2🤯2💅2
Делюсь бесполезными знаниями.
На x86 код
И
Выполняются одинакого быстро. Это бесполезное знание.
Полезное знание тут заключается в том, что, оказывается, бенчмарки можно запускать в godbolt. (Бенчмарк не мой, подглядел в другом месте). (А ещё на самом деле в более сложной функции relaxed допускает переупорядочивание недоступное при seq но это вообще сложные материи и не сумел сконструировать пример, который бы это воспроизводил).
Да, аппаратная инструкция может задавать более серьезное упорядочивание, чем требует модель памяти. Тут должны быть ссылки на реальные документы и доказательства, но хей, мы тут деградацией занимаемся, а не образованием.
Ну вот теперь мы это знаем. Что же это нам дает? Да ничего не дает.
Если мы походим по популярным проектам, то увидим, что везде fetch_add используется с relaxed.
PyTorch
Folly
LevelDB
Boost
gRPC
Скорее всего потому, что библиотеки популярные, и используются на разных архитектурах.
Но пусть я делаю что-то только для x86. Допустим я разобрался, и умею читать всякое такое (а я не умею, я - деградарий):
Есть ли у меня мотивация писать
???
Никакой мотивации. Я напишу relaxed просто на всякий случай. А мало ли что.
Получается, что знание, что в данном случае оно не нужно - просто бесполезное. Ну не узнал я что-то, и фиг бы с ним. Зачем я потратил на это столько времени? Для чего, мистер Андерсен?
И как бы мне получать полезные знания, не тратя время на бесполезные?
А тут ещё эти ваши нейронки, дискредетирующие знания в голове вцелом... Нипанятна...
На x86 код
counter.fetch_add(1, std::memory_order_relaxed);
И
counter.fetch_add(1);
Выполняются одинакого быстро. Это бесполезное знание.
Полезное знание тут заключается в том, что, оказывается, бенчмарки можно запускать в godbolt. (Бенчмарк не мой, подглядел в другом месте). (А ещё на самом деле в более сложной функции relaxed допускает переупорядочивание недоступное при seq но это вообще сложные материи и не сумел сконструировать пример, который бы это воспроизводил).
Да, аппаратная инструкция может задавать более серьезное упорядочивание, чем требует модель памяти. Тут должны быть ссылки на реальные документы и доказательства, но хей, мы тут деградацией занимаемся, а не образованием.
Ну вот теперь мы это знаем. Что же это нам дает? Да ничего не дает.
Если мы походим по популярным проектам, то увидим, что везде fetch_add используется с relaxed.
PyTorch
Folly
LevelDB
Boost
gRPC
Скорее всего потому, что библиотеки популярные, и используются на разных архитектурах.
Но пусть я делаю что-то только для x86. Допустим я разобрался, и умею читать всякое такое (а я не умею, я - деградарий):
define void @inc1(ptr nocapture %p) nounwind ssp {
; CHECK-LABEL: inc1:
; CHECK: # %bb.0: # %entry
; CHECK-NEXT: lock incl (%rdi)
; CHECK-NEXT: retq
;
; SLOW_INC-LABEL: inc1:
; SLOW_INC: # %bb.0: # %entry
; SLOW_INC-NEXT: lock addl $1, (%rdi)
; SLOW_INC-NEXT: retq
entry:
%0 = atomicrmw add ptr %p, i32 1 monotonic
ret void
}
Есть ли у меня мотивация писать
counter.fetch_add(1);
???
Никакой мотивации. Я напишу relaxed просто на всякий случай. А мало ли что.
Получается, что знание, что в данном случае оно не нужно - просто бесполезное. Ну не узнал я что-то, и фиг бы с ним. Зачем я потратил на это столько времени? Для чего, мистер Андерсен?
И как бы мне получать полезные знания, не тратя время на бесполезные?
А тут ещё эти ваши нейронки, дискредетирующие знания в голове вцелом... Нипанятна...
godbolt.org
Compiler Explorer - C++ (x86-64 clang 18.1.0)
// TITLE: Atomic Increment Ordering: relaxed vs. seq_cst
// QUESTION: Which snippet is faster? (Assume x86-64)
// ANSWER_START
// Both snippets perform the same on x86. The x86 LOCK prefix required for
// fetch_add provides acquire-release semantics regardless…
// QUESTION: Which snippet is faster? (Assume x86-64)
// ANSWER_START
// Both snippets perform the same on x86. The x86 LOCK prefix required for
// fetch_add provides acquire-release semantics regardless…
💊19❤4😁4🤔2👍1