Немного туп(л,)
84 subscribers
179 photos
26 videos
40 links
Маленький бложег С++ программиста, увлекающегося Rust'ом. Не столько про пргрмрвне, а вообще.
Download Telegram
#irl

Вот сидишь ты такой вне дома. Дома окажешься хз когда. На телефоне 8% зарядки осталось. Но у тебя же есть powerbank! Вот только кабель зарядный ты дома забыл 🥲
#haha #rust

Я тут между делом пилю свою плагиновую систему на расте. Главная её идея - safety. То есть, тот же (самый популярный) libloading - полностью unsafe, и от этого я хочу уйти. Если вкратце, уход от unsafe достигается за счёт добавления в плагин дополнительных метаданных и их проверке при загрузке. Ну как "достигается". Будет "достигаться", если я это дело допилю, потому что оно пока очень unsound :) Ломаю safe гарантию полностью!

Но суть вот в чём. Изначально я дал этому проекту название "plunger", потому что созвучно с "plugin", а я люблю созвучия и игру слов. Глянул в переводчик, он даёт перевод "поршень", меня устроило. При этом внутри я использовал всё тот же libloading, ибо зачем выдумывать лишнее, если база уже есть? А поддержку MacOS можно было бы и позже законтрибьютить. Но тут я столкнулся с нюансом, который мне не нравится совсем. И было принято решение запилить свою реализацию, благо это не rocket science. А для реализации надо придумать название. И пошёл я гуглить, что бы такое похожее на "поршень" придумать. И выяснил, что "plunger", с точки зрения англо-говорящего человека, это всё-таки не столько "поршень", сколько "вантуз". Не очень удачное имечко, для библиотеки 🌚

Штош, пойду искать что-то другое. Если кто предложит варианты, я только за 😊

P.S. К слову, может я ещё себя пересилю и не буду делать велосипед, а всё-таки обойдусь с помощью libloading, но имя проекту менять точно надо 😅
#irl

Я прокрастинатор. Одним из симптомов является огромный список для чтения. Например, на Хабре у меня почти полторы тысячи статей в закладках.
Кроме того я часто чем-то "загораюсь" и по-быстренькому вкатываюсь в тему, после чего, оценив объёмы материала, оставляю его "на потом".
Ещё мне часто приходится (по работе) разбираться с одной задачей, потом резко переключиться на другую, потом на третью. Не всегда это прям по рабочим вопросам, иногда просто интерес к чему-то возникает. И из-за этих переключений я не изучаю тему до конца, и оставляю материал "на потом".

Это я всё к чему. У меня в браузере телефона было 380 открытых вкладок. Триста восемьдесят, Карл! На телефоне! Года три, как я их "копил". В прошлом году, если не путаю, их стало 200 и я почистил, прочитав мелкое и удалив лишнее. А сейчас 380... И я себя пересилил. И удалил всё. А то браузер уж совсем начал лагать 🌚
#jobbing #truestory

Устоявшиеся правила надо соблюдать. Даже если они вам не нравятся. Их. Надо. Соблюдать. Так, о чём это я?

Сейчас мне приходится заниматься документацией. Ну, вот так получилось, оставим в стороне вопрос, почему это делаю я, а не специально обученные люди из методического отдела. Делается это всё в Word'е. Хорошее ПО, одно из лучших в своём классе, тут спорить глупо. Но не без проблем.
Вот одна из проблем сейчас и вылезла. Он завис. Ну, завис и завис, бывает, где-то ресурсы не почистили за собой, где-то out of bound допустили, всё понимаю, бывает. Да и разве ж это проблема? Инстинкт автосохранения у меня есть, Ctrl+S каждые пару минут на автомате, даже не задумываясь. Поэтому можно прибить процесс и запустить заново, потеряв всего пару минут работы, ведь так?

Нет. Не так. Начиная с Word 2013, выпущенной в 2012 году, для сохранения файла надо использовать сочетание Shift+F12, а не классическое Ctrl+S. И плевать на долгие годы создания пользовательского опыта, плевать на мнемонику "Save" у буквы 'S', которая раньше даже сбоку клавиши писалось (на старых клавиатурах). К чёрту всё!

Что, простите? Автосохранение? Так оно включено. Но в этот раз почему-то не сработало. Может быть потому что у меня был открыт параллельно файл с таким же именем, а может ещё что-то, не знаю. Но факт остаётся один: из-за "гениальности" какого-то UX-специалиста я потерял полтора часа работы. Желание что-либо делать пропало совсем.

Да, эти сочетания можно отредактировать. Но Word не мой основной инструмент, поэтому мне в его настройки лезть и не приходилось никогда.

Поэтому, пожалуйства, соблюдайте устоявшиеся правила. В мусульманских странах одевайтесь подобающе, в православный храм не ходите в шортах, а при разработке ПО используйте устоявшиеся сочетания клавиш для совершения классических действий.
#haha #irl #truestory

Зарплата инженера-программиста, пишущего на языке С++, middle уровня 🌚
Ну ладно, это аванс за половину июня 🥲
#repost

Это очень интересно.
☘️Британские учёные доказали, что люди славные котики
Что я наисследовала, пока изучала общение людей с голосовыми помощниками

Если вдруг вы помните, я планировала написать диплом об особенностях общения живых мясных людей с голосовыми помощниками. И даже собирала для этого дела респондентов — людей, которые общаются со своими Алисами, Марусями и Сири — или наоборот, из принципа не общаются. Щас расскажу, что я узнала.

☘️
Изначально у меня была идея выявить, что же такого в действиях Алисы заставляет пользователей воспринимать её как человека и так по-человечески на неё реагировать: например, защищать от обидчиков, благодарить или спрашивать, как у нее дела. Но с этой затеей меня остановила ноучница, потому что а) с Алисой всё понятно — она скушала много человеческих текстов, и на них хорошо научилась имитировать произвольный диалог [Вот тут можно об этом у Яндекса почитать]. Это и заставляет. б) интереснее будет проверить, что именно внутри людей сподвигает их очеловечивать железяку — какие их личностные особенности. в) на психфаке в дипломе нада исследовать людей, не-людей — ненада:)

☘️
Как говорится, у нас было 40 респондентов, 3 методики, анкета на 130+ вопросов и несколько гипотез. Не то, чтобы этого было достаточно для нормального исследования, но если решил защищать диплом, иди в своем решении до конца.

Короче, я придумала 15 утверждений, которые с разных сторон показывают особенности общения с голосовым помощником: нравится ли людям с ним болтать, испытывают ли они вину за грубость, важно ли для них, какое «мнение» сложится о них у голосового помощника и пр. — и попросила респондентов оценить, насколько они согласны или нет с этими утверждениям. А потом я замерила их уровень эмоционального интеллекта, сопереживания и социального самоконтроля по готовым методикам. И посмотрела, насколько эти штуки взаимосвязаны.

Вот что получилось:

🔹люди с высоким уровнем сопереживания чаще стремятся защищать голосового помощника, если кто-то ему грубит при них. А ещё всегда извиняются перед своими Алисами, если нагрубили сами. С этого вопроса, кстати, идея исследования и родилась.
Я всё хотела понять, что же в действиях Алисы заставляет людей защищать её — хотя очевидно же, что она, бесчувственная железяка, в защите не нуждается. Ну вот и ответ. Дело не в действиях Алисы, а в том, что люди — хорошие и сопереживающие котики. Теперь у меня есть данные, которые это подтверждают!

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

🔹зато большое значение имеет уровень контроля экспрессии — насколько хорошо человек контролирует проявление своих эмоций. Так вот. Людей, которые хорошо конролируют свои эмоции бесит, когда голосовой помощник выходит за рамки делового общения — шуточки там шутит, дерзит. А действительно, хули это Сири себе позволяет, если я — нет?!

🔹а ещё людей с высоким уровнем контроля экспресии тоже бесит, когда кто-то грубит голосовым помощникам при них. Но видимо, не потому что они такие сопереживающие, а потому что ожидают от людей того же, что делают сами — контроля [blyat’] своих [suka!] эмоций.

Ещё я немного посравнивала, как влияют на общение с голосовыми помощниками всякие там социально-демографические факторы: пол, возраст, наличие отношений:

🔹пол не влияет вообще никак.

🔹а вот отсутствие отношений/брака влияет: люди, у которых нет партнёра, чаще болтают с голосовыми помощниками просто так. Но испытывают стыд и неловкость, если железяка делает им комплименты [это очень логично, но всё равно от этих данных мне стало немного грустно]

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

Такие дела. Надеюсь, было познавательно:)

[Передаю много больших спасибов всем 40 человекам, которые ответили на мою анкету.
И особенно большое спасибо Саше, который посчитал мне эти данные своим питоном
]
Моя жена на мой канальчик не подписана, пусть хоть так на него "посмотрит" 😊
#c #prog

Пост специально для Вафеля.

Преамбула:
У нас в кодах есть очередь. Она предназначена для работы из двух потоков: один поток пишет в очередь, другой из неё читает.
При этом в ней не используются никакие примитивы синхронизации. И вот не все верят в то, что это может работать гарантированно и безопасно.
Сейчас я покажу, что это за очередь и объясню как она работает, и почему это безопасно. Единственное, чтобы соблюсти все правовые аспекты, я рассмотрю не конкретно ту очередь, что используется у нас, а напишу вольную интерпретацию идеи на чистом Си.
К слову, идея очень просто и элегантна и очень мне нравится. Вполне вероятно, что этот код изначально был взят из книг (Александреску, или что-то подобное), но уверенности у меня в этом нет, только подозрение.

Так как переписываем на Си, и шаблонов тут нема, то в качестве данных будем использовать обычный int.
Итак, нам нужна очередь. Простейший способ оформить очередь - односвязный список. Так и сделаем:
struct Item {
int data;
struct Item* next;
};

Так же добавим для удобства методы для создания и удаления Item'а:
struct Item* new_item(int data) {
struct Item* item = (struct Item*)calloc(1, sizeof(struct Item));
item->data = data;
return item;
}
void free_item(struct Item* item) {
free(item);
}

Обращаю внимание, что мы не следим за корректностью выделения памяти и проверкой входных данных на NULL, ибо это всего лишь пример.

Что же, вроде бы очередь фактически готова, но пока не понятно, как оно может предоставлять гарантии безопасности и отсутствия гонок данных при конкурентном доступе. А всё потому, что чтобы магия заработала, нам потребуется ещё одна небольшая структурка:
struct Queue {
struct Item* head;
struct Item* tail;
}
struct Queue* queue_init() {
struct Queue* queue = (struct Queue*)malloc(sizeof(struct Queue));
struct Item* item = new_item(0);
queue->head = item;
queue->tail = item;
return queue;
}

В примере выше нет функции-деструктора, потому что в нём будет некоторая хитрость, и рассмотрим мы его позже.
Немного туп(л,)
#c #prog Пост специально для Вафеля. Преамбула: У нас в кодах есть очередь. Она предназначена для работы из двух потоков: один поток пишет в очередь, другой из неё читает. При этом в ней не используются никакие примитивы синхронизации. И вот не все верят…
Итак, мы уже имеем две структуры: одна представляет собой элемент очереди, вторая описывает "всю" очередь с помощью указателей на хвост и голову списка. Казалось бы, зачем в односвязном списке указатель на хвост, но ответ очень прост. И на самом деле это ответ на ещё один вопрос: "А как нам определить, пустая очередь или нет?" А вот так:
int queue_is_empty(struct Queue* queue) {
return queue->head == queue->tail;
}

Таким образом, уже понятна одна хитрость, используемая в этой очереди: мы используем один Item, как "заглушку". Этот элемент должен быть в очереди всегда и он всегда будет пустым, без валидных данных. Но именно благодаря этой заглушке, мы можем спокойно оперировать с очередью, не боясь словить data-race на операциях записи и чтения. Продемонстрирую:
void queue_push(struct Queue* queue, int data) {
struct Item* item = new_item(data); // (1)
queue->tail->next = item; // (2)
queue->tail = item; // (3)
}
bool queue_pop(struct Queue* queue, int* data) {
if (queue_is_empty(queue)) // (4)
return 0;

strut Item* old_head = queue->head; // (5)
queue->head = old_head->next; // (6)
free_item(old_head); // (7)

*data = queue->head->data; // (8)
return 1;
}

Всё. Наша очередь полностью готова. Весь код выше занял 42 (don't panic!) строки. И он красив и изящен, как мне кажется. Но если читатель со мной ещё не согласен, то объясню последний листинг:
Немного туп(л,)
Итак, мы уже имеем две структуры: одна представляет собой элемент очереди, вторая описывает "всю" очередь с помощью указателей на хвост и голову списка. Казалось бы, зачем в односвязном списке указатель на хвост, но ответ очень прост. И на самом деле это ответ…
(1) Мы создаём новый элемент в потоке писателя. В этом время поток-читатель может спокойно оперировать с очередью как хочет, ведь мы ещё не тронули память, которая "пошарена" между потоками. Кстати это может быть совершенно не атомарная операция, и если data будет посложнее, чем int, то всё равно ничего не изменится.
(2) Вот тут мы уже трогаем шареную память и де-факто изменяем хвост, сделав очередь длиннее на один элемент. Однако, посмотрим на (4) и вспомним, что, для проверки "пустоты" очереди, поля tail'а не используются от слова совсем. Используется значение самого указателя tail, а мы его ещё не меняли. То есть, несмотря на то, что мы уже вроде бы меняем общую для потоков память, на самом деле, мы меняем "невидимую" для потока-читателя память. То есть читатель может в этот же момент времени начал обрабатывать данные в tail, но мы ему не помешаем.
(3) А вот тут мы уже меняем указатель хвоста и наши добавленные данные становятся видимыми для потока-читателя.
(4) Здесь мы уже в потоке читателя проверяем очередь на пустоту, сравнивая указатели начала и конца очереди. Как я говорил в (2), мы можем начать обрабатывать данные в последнем элементе списка, пока писатель добавляет новые данные в нашу очередь (на самом деле это будет происходить в пунктах 5-8). Ещё возможен вариант, что head == tail, но писатель уже добавил данные в tail->next, но не успел изменить сам tail. Тогда всё тоже безопасно, мы просто вывалимся из функции. Ну а вариант, пока мы читаем далеко от хвоста - неинтересен.
(5) Тут мы берём нашу "старую" голову списка. Напомню (см. функцию queue_init), что первый элемент всегда заглушка. В нём нет валидных данных. Поэтому мы берём этот элемент, в (6) смещаем голову, чтобы она указывала на item с валидными данными и в (7) дропаем наш элемент-заглушку.
(8) А тут мы уже читаем валидные данные очереди и возвращаем их в вызывающий код (через указатель data). При этом, обращаю внимание на два момента:
1. Мы читаем из данных, которые не трогаются потоком-писателем. Он эту область сформировал ещё в (1), пока мы не имели сюда доступа, и больше не трогает этот участок памяти.
2. После того, как мы прочитали данные, эти данные становятся невалидными. Их нельзя читать второй раз. Будь здесь что-то сложнее, чем int, например указатель на сложную структуру, то стоило бы этот указатель занулить. Или если бы это была сама сложная структура (а не указатель на неё), то как-то её уничтожить. В любом случае это уже специфика конкретных данных, которые будут в этой очереди храниться. Если бы я приводил пример на C++ с шаблонным кодом, то да, я бы записал здесь, например queue->head->data = T {}, или что-то подобное.

А, ну и я забыл ещё кое-что, функцию уничтожения очереди (на самом деле я её забыл специально, иначе не получилось бы 42 строки):
void queue_free(struct Queue* queue) {
int tmp;
while(queue_pop(queue, &tmp));
free_item(queue->head);
free(queue);
}

Вот так, красиво и аккуратно мы обходим data races и memory corruption, если соблюдаются два инварианта:
1. Только один поток пишет.
2. Только один поток читает.
Ну и, исходя из написанного кода функции-деструктора: уничтожение очереди = чтение из очереди. Правда если вы пытаетесь уничтожить очередь и параллельно с ней работать (читать или писать - не важно), то вы делаете что-то неправильное.
Немного туп(л,)
(1) Мы создаём новый элемент в потоке писателя. В этом время поток-читатель может спокойно оперировать с очередью как хочет, ведь мы ещё не тронули память, которая "пошарена" между потоками. Кстати это может быть совершенно не атомарная операция, и если data…
Однако образованный читатель может начать кричать: "Здесь нет гарантий, что присваивание это атомарная операция, поэтому всё летит в тар-тарары!" И будет прав. На это я отвечу следующим образом:
Я не просто так решил написать пример на Си. Когда пишешь на Си, ты всё-таки должен знать и понимать особенности архитектуры, под которую ты пишешь код. Мы пишем код под x86_64. И на этой архитектуре операция присваивания для указателей - атомарна. Более того, я проверял на godbolt (правда давно уже, года полтора-два назад), и, если я правильно путаю, только под AVR это была неатомарная операция.

Тогда ещё более образованный читатель скажет: "Хорошо, допустим на этих архитектурах это безопасно, но так как ты не используешь синхронизацию, то может пройти уйма времени, прежде чем данные, добавленные потоком-писателем попадут в кэш ядра процессора, откуда их сможет достать поток-читатель". Что же, абсолютно корректное замечание, на что я отвечу:
Насрать! Нет, серьёзно, абсолютно пофигу. Если вернуться к функциям queue_push и queue_pop, то уже видно, что возможно вываливание из цикла, когда в очереди уже "технически" есть данные. То есть не стоит рассматривать этот код как "безусловная немедленная передача данных между потоками" (хотя в реальности оно так работает, но гарантий этого я не дам), а скорее как "передача информационных сообщений между потоками". То есть эта очередь гарантирует целостную доставку, но не гарантирует оперативность доставки. А ещё она lock-free и wait-free.

Ну и напоследок, маленький пример использования этой очереди с использованием стандарта C11 (потому что там уже есть стандартизированные потоки):
int* foo(void* vqueue) {
struct Queue* queue = (struct Queue*)vqueue;

for(int i = 0; i < 100; ++i) {
queue_push(queue, i);
}

return thrd_success;
}
int* bar(void* queue) {
struct Queue* queue = (struct Queue*)vqueue;

int data;
while(queue_pop(queue, &data)) {
printf("Received: %d\n", data);
}
printf("end");

return thrd_success;
}

int main(int argc, char** argv) {
struct Queue* queue = queue_init();

thrd_t producer, consumer;
thrd_create(&producer, (thrd_start_t)foo, (void*)queue);
thrd_create(&consumer, (thrd_start_t)bar, (void*)queue);

int thrd_res;
thrd_join(producer, &res);
thrd_join(consumer, &res);

queue_free(queue);
return 0;
}
Немного туп(л,)
Однако образованный читатель может начать кричать: "Здесь нет гарантий, что присваивание это атомарная операция, поэтому всё летит в тар-тарары!" И будет прав. На это я отвечу следующим образом: Я не просто так решил написать пример на Си. Когда пишешь на…
На самом деле тут ещё один момент, который я хочу осветить.

В беседе, которая вылилась в предыдущей пост, я упоминал, что раньше у нас были атомарные счётчики в этой очереди, которые я удалил и получил примерно 5% по перфу.
Эти счётчики позволяли вычислить длину очереди. И с ними структура Queue выглядела бы ("бы" потому что напомню, что я здесь выкладываю не оригинальный плюсовый код, а его интерпретацию на Си) примерно так:
struct Queue {
struct Item* head;
struct Item* tail;
atomic_ullong count_push;
atomic_ullong count_pop;
}


Соответственно, в момент инициализации очереди эти счётчики занулялись, а функции добавления и извлечения элементов из очереди выглядели так:
void queue_push(struct Queue* queue, int data) {
struct Item* item = new_item(data);
queue->tail->next = item;
queue->tail = item;
++count_push; // <-- this
}
bool queue_pop(struct Queue* queue, int* data) {
if (queue_is_empty(queue))
return 0;

strut Item* old_head = queue->head;
queue->head = old_head->next;
free_item(old_head);

*data = queue->head->data;
++count_pop; // <-- this
return 1;
}


Как можно заметить, счётчики только увеличиваются, и никогда не уменьшаются. Это нормально, потому что счётчики беззнаковые, а согласно стандарту:
Unsigned integer arithmetic is always performed modulo 2^n where n is the number of bits in that particular integer. E.g. for unsigned int, adding one to UINT_MAX gives 0, and subtracting one from 0 gives UINT_MAX.

Тогда функцию для получения длины очереди можно написать так:
unsigned long long queue_length(struct Queue* queue) {
unsigned long long push = atomic_load(queue->count_push); // (1)
unsigned long long pop = atomic_load(queue->count_pop); // (2)
return push - pop;
}

В этой операции опять же возможны переполнения, но суть - всё равно, ибо смотри выдержку из стандарта.
Единственное рассмотрим нюансы атомарных операций:
(1) Мы не(!)атомарно разыменовываем указатель и потом уже (!)атомарно читаем значение счётчика. Тут всё нормально, потому что указатель на queue никто не меняет, а чтение и запись атомарного счётчика атомарны (sic!), поэтому гонок с функцией queue_push не будет.
(2) Прежде чем мы дойдём до этой строки может выполниться куча операцией push/pop, но нам это не важно!
Мы опять неатомарно разыменовываем указатель и атомарно читаем значение счётчика pop. Ну и потом уже неатомарно посчитаем разность этих счётчиков из закэшированных значений, поэтому нам неважно, происходят ли ещё какие-то push/pop.
Немного туп(л,)
На самом деле тут ещё один момент, который я хочу осветить. В беседе, которая вылилась в предыдущей пост, я упоминал, что раньше у нас были атомарные счётчики в этой очереди, которые я удалил и получил примерно 5% по перфу. Эти счётчики позволяли вычислить…
Добавлю ещё два нюанса:
1. Так как наши коды очень старые, изначально счётчики были не атомарные, а volatile, что как бы важно, но не очень (объяснение будет дальше)
2. Оригинальная функция так же учитывала проверку на переполнение, когда count_push уже переполнился, а count_pop ещё нет, и выглядело это примерно так:
unsigned long long queue_length(struct Queue* queue) {
unsigned long long push = atomic_load(queue->count_push);
unsigned long long pop = atomic_load(queue->count_pop);

if (pop > push)
return ULLONG_MAX - pop + push + 1
else
return push - pop;
}

Но мне кажется эта проверка бессмысленной, так как:
1. От перемены мест слагаемых сумма не меняется
2. ULLONG_MAX + 1 = 0
3. 0 - pop + push = push - pop
И в итоге получается то же самое, что и во второй ветке условия. Но если я не прав, будет отлично если кто-нибудь меня поправит и объяснит где я ошибаюсь.

Теперь надо понять, почему удаление этих счётчиков увеличило перформанс данного кода.
Нужно понять, что такое "атомарная операция". Это операция, которая выполняется неделимо, пока выполняется она - не выполняется ничего другого. Однако, atomic-типы в C++ и Си делают по-умолчанию больше: они учитывают memory_ordering, то бишь порядок доступа к памяти. Тут я ступаю на рыхлую почву, потому что сам всё ещё несколько плаваю в этой теме. Поэтому углубляться я не буду, чтобы сильно не наврать, но грубо постараюсь объяснить.
Поведение по-умолчанию для всех атомарных операций - memory_order_seq_cst, что для операций чтения эквивалентно memory_order_acquire, а для операций записи - memory_order_release. Read-modify операции я в примерах выше не использовал, поэтому опустим их.
memory_order_acquire значит, что в данном участке памяти нельзя менять порядок других операций чтения или записи до завершения текущей операции чтения. То есть наш поток увидит все изменения, совершённые другими потоками.
memory_order_release значит, что в данном участке памяти нельзя менять порядок других операций чтения или записи после завершения текущей операции записи. То есть все потоки увидят эту запись.
Таким образом, операции со счётчиками добавляют синхронизацию в наш код расставляя барьеры памяти, что легко увидеть в ассемблерном выхлопе (спасибо godbolt):
// Оптимизации -O3
int* foo(void* v) {
int* a = (int*)v;
++(*a);
return thrd_success;
}
int* bar(void* v) {
atomic_ullong* a = (atomic_ullong*)v;
++(*a);
return thrd_success;
}

foo:
add DWORD PTR [rdi], 1
xor eax, eax
ret
bar:
lock add QWORD PTR [rdi], 1
xor eax, eax
ret
Немного туп(л,)
Добавлю ещё два нюанса: 1. Так как наши коды очень старые, изначально счётчики были не атомарные, а volatile, что как бы важно, но не очень (объяснение будет дальше) 2. Оригинальная функция так же учитывала проверку на переполнение, когда count_push уже переполнился…
При этом можно, казалось бы, использовать явно memory_order_relaxed, но это всё равно бьёт по перфу: relaxed операции не добавляют синхронизации, НО всё равно заставляют компилятор вызвать сброс кэша процессора, что в минималистичном примере даёт такой же результат для функции bar:
int* foo(void* v) {
atomic_ullong* a = (atomic_ullong*)v;
atomic_fetch_add_explicit(a, 1, memory_order_relaxed);
return thrd_success;
}

foo:
lock add QWORD PTR [rdi], 1
xor eax, eax
ret

То есть, атомарные операции в С/С++ это больше, чем просто "атомарные" операции, это ещё и полноценная синхронизация, как минимум между кэшами ядер.

А когда мы используем volatile вместо атомарных типов, то тут два варианта:
1. Компилятор сгенерирует такой же код, как для memory_order_relaxed операции (и так точно делает MSVC, если не всегда, то часто)
2. Компилятор сгенерирует "нормальный" код без сброса кэша, и поэтому ситуация будет полностью аналогична тому, что происходит с присваиванием указателей (см. предыдущие посты).
#irl #hate
БЛЯДЬ КАКОЙ ЖЕ УЕБАНСКИЙ ТЕКСТОВЫЙ РЕДАКТОР У ТЕЛЕГРАММА, СУКА, ПОСТОЯННО ВЁРСТКА КОДА СЛЕТАЕТ БЛЯДЬ Я ПОЛ ЧАСА ТУПО КОПИРОВАЛ ЭТУ ТЕЛЕГУ ТЕКСТА ИЗ ТЕКСТОВОГО ФАЙЛА
😁1
This media is not supported in your browser
VIEW IN TELEGRAM
#irl
Моя супруга приготовила торт ^_^ Но я его вам не дам :P
😁1
Раз уж я показал, чем занимается моя супруга, то расскажу ещё чем я недавно занимался.

#prog #rust
Решил я что-нибудь простенькое растовое запилить для наполнения портфолио. Чисто чтобы хоть что-то показать.
Сделал я две штуки:

1. Есть одна нерешённая математическая проблема (одна из, их на самом деле целое море) - гипотеза Коллатца. Гипотеза гласит, что абсолютно любое натуральное число n сведётся к 1, если применять к нему следующие преобразования: если число чётное - делим его на 2, в противном случае - умножаем на 3 и прибавляем 1. Так же эта гипотеза называется "3n+1 дилемма", или "сиракузская проблема" (потому что такая последовательность чисел, построенная в результате этих преобразований, называется "сиракузской").
Это нерешённая проблема математики, потому что не получается математически доказать, что она верна, и не получается найти контр-пример, чтобы доказать, что она не верна.

Сопсна, что сделал я? Да особо ничего выдающегося, простенький трейт-итератор, который позволяет для любого числа вызвать метод collatz_iter(), по которому можно проитерироваться по всей сиракузской последовательности для этого изначального числа. То есть написать что-то такое:
for n in 8.collatz_iter() {
print!("{} ", n);
}

что выведет:
8 4 2 1

Чтобы было поинтереснее, я посыпал сверху немного macro_rules!, cargo-фичи, Resultы, ну и сопсна всё.

Полистать код (его совсем не много) можно здесь.
Немного туп(л,)
Раз уж я показал, чем занимается моя супруга, то расскажу ещё чем я недавно занимался. #prog #rust Решил я что-нибудь простенькое растовое запилить для наполнения портфолио. Чисто чтобы хоть что-то показать. Сделал я две штуки: 1. Есть одна нерешённая математическая…
2. Вторая штука позабористей, да и делать я её начал раньше, но всё-таки она оказалась посложнее, и код там сейчас очень не очень 🌚

Возьмём число π (Пи). Это бесконечная непериодическая десятичная дробь. Раз она бесконечная и непериодическая, значит в её записи (после запятой) можно найти абсолютно любое другое число. А значит, что можно взять любой файл с вашего компуктера, представить это байты в виде числовой последовательности, потом найти эту числовую последовательность где-то в числе π, и запомнить позицию первой найденной цифры и длину этой последовательности. То есть, условно, превратить ваш гигабайтный видосик в два числа.
На основе всего вышеперечисленного реализован проект πfs. То есть это файловая система, которая "на лету" ищет ваши файлы в числе π и сохраняет два числа: позицию и длину.

Собственно я посмеялся, потому что с одной стороны это очень круто, с другой стороны нифига не практично (ибо считаться этот ваш фильм на пару гигабайт будет миллионы лет). Но подумал: а можно ли сделать лучше? Ибо вот, например, пишу я код, часто меняю один и тот же файл, и его постоянно приходится пересчитывать в этом числе π. Лишний жор ресурсов! А что если сделать... архиватор? Ведь файлы архивируются не так уж часто, значит можно потратить время и посчитать нужные циферки, тем более что весь этот процесс можно распараллелить на тысячи мощных ЭВМ, и архивировать таким образом особо важные файлы, которые терять нельзя. И... Да, я сделал свой архиватор на базе числа π.

Ну, каааааак "сделал". Это слишком громко сказано. Но я разработал спецификацию формата архива (пока черновую), которая позволяет хранить файлы в виде двух чисел (позиция + длина), дополнительно есть место для кое-какой мета-информации, так же предусмотрено, что файлы можно бить на чанки разной длины (ибо маленький чанк в числе π может быть найти проще, чем весь файл), и, самое главное, предусмотрел следующее:

Если в каком-то далёком апокалиптическом будущем мы останемся без компьютерной техники, то обычный математик, имея на руках лист бумаги, карандаш и распечатанный файл архива, сможет расшифровать этот файл и получить исходную последовательность байт! Что делать с этими байтами, не имея компьютера - этот вопрос давайте пока что опустим.
Круто же? Круто!

Но спецификация явно сырая, архиватора ещё нет, НО (sic!) есть разархиватор! Правда у него много ограничений, например я решил начать работать преимущественно на целых числах, ибо это быстрее, поэтому если пытаться посчитать "далёкую" цифру числа π, то очень быстро происходит переполнение и паника. В общем код там откровенное говно, но спецификация мне более-менее нравится. Дошлифовка явно нужна, но уже вполне жизнеспособно, кмк.

Собственно, репозиторий здесь (но про качество кода я предупредил).
#irl

Хозяйке на заметку:
Если высокая температура долго не сбивается обычными средствами на базе парацетамола, можно принять ибуклин. Ибуклин можно купить как готовый препарат, а можно сделать самостоятельно, ибо это 400 мг ибупрофена и 325 мг парацетамола. Всё, остальные вспомогательные вещества в составе - это оболочка таблетки, наполнители для увеличения объёма таблетки, подкрашивание и так далее.

Собственно, "приготовить" ибуклин дома легко, если есть парацетамол и ибупрофен в "чистом" виде. Но есть нюанс.
Ибупрофен обычно дозируется кратно 200 мг на таблетку (я встречал дозировки только 200 мг и 400 мг), так что "собрать" 400 мг будет легко. А вот с парацетамолом чуть сложнее.

Стандартная дозировка парацетамола в таблетках - 500 мг на 1 таблетку. 500 мг делить на 325 мг не очень удобно. В принципе, когда мне это надо я не в состоянии считать, как там надо дробить таблетку, поэтому пью целую. И вот не советую так делать. ОСОБЕННО если до этого вы пили некоторое другое средство на базе парацетамола. По-хорошему это вам надо:
1. Определить, сколько часов/минут назад было выпито средство
2. Найти инструкцию к выпитому препарату и посмотреть, сколько в его составе парацетамола
3. Запомнить (или скорее нагуглить), что период полувыведения парацетамола у взрослых составляет 2,7 часов и что выводится оно в основном с мочой.
4. Посчитать, сколько парацетамола осталось в вашем организме.
5. Скорректировать количество парацетамола, которое надо сейчас выпить.

А то если этого не сделать, будете как я: вчера весь день лежал с температурой 38,5, не сбивалась никак, пил парацетамоловые порошочки и спал. В итоге вечером плюнул и "приготовил" себе ибуклин. Так как с температурой думать толком сил не было, никаких пересчётов не производил и хряпнул 500 мг парацетамола + 400 мг ибупрофена. Когда проснулся на градуснике было 36,1, что тоже, знаете ли, не очень хорошо. По идее, сбивать температуру надо до 37,5, не ниже. И сейчас объясню почему.