LINUX.ORG.RU

Метапрограммирование на препроцессоре

 , , , , санитары


1

6

Привет, ЛОР!

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

Факториал в функциональном стиле:

#define factorial(n)          ML99_natMatch(n, v(factorial_))
#define factorial_Z_IMPL(...) v(1)
#define factorial_S_IMPL(n)   ML99_mul(ML99_inc(v(n)), factorial(v(n)))

ML99_ASSERT_EQ(factorial(v(4)), v(24));

Алгебраические типы:

#include <datatype99.h>

datatype(
    BinaryTree,
    (Leaf, int),
    (Node, BinaryTree *, int, BinaryTree *)
);

int sum(const BinaryTree *tree) {
    match(*tree) {
        of(Leaf, x) return *x;
        of(Node, lhs, x, rhs) return sum(*lhs) + *x + sum(*rhs);
    }

    return -1;
}

Интерфейсы (почти как трейты в Rust):

#include <interface99.h>

#include <stdio.h>

#define Shape_IFACE                      \
    vfunc( int, perim, const VSelf)      \
    vfunc(void, scale, VSelf, int factor)

interface(Shape);

typedef struct {
    int a, b;
} Rectangle;

int  Rectangle_perim(const VSelf) { /* ... */ }
void Rectangle_scale(VSelf, int factor) { /* ... */ }

impl(Shape, Rectangle);

typedef struct {
    int a, b, c;
} Triangle;

int  Triangle_perim(const VSelf) { /* ... */ }
void Triangle_scale(VSelf, int factor) { /* ... */ }

impl(Shape, Triangle);

void test(Shape shape) {
    printf("perim = %d\n", VCALL(shape, perim));
    VCALL(shape, scale, 5);
    printf("perim = %d\n", VCALL(shape, perim));
}

Я считаю, на этом все попытки убить Сишечку можно закапывать. Ссылка: https://github.com/hirrolot/metalang99

★★★★★

Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от tinykey

volatile — это не атомики. В стандарте написано, что volatile можно использовать только для ловли сигналов, и то для этого требуется использовать тип volatile sig_atomic_t.

Атомики есть в стандартах C11 и C++11. В более ранних нужно использовать расширения GNU C: __sync_* и __atomic_*.

shdown
()
Ответ на: комментарий от shdown

volatile — это не атомики. В стандарте написано, что volatile можно использовать только для ловли сигналов, и то для этого требуется использовать тип volatile sig_atomic_t.

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

#define READ_ONCE(x)							\
({									\
	compiletime_assert_rwonce_type(x);				\
	__READ_ONCE(x);							\
})

#define __WRITE_ONCE(x, val)						\
do {									\
	*(volatile typeof(x) *)&(x) = (val);				\
} while (0)

tinykey
()
Ответ на: комментарий от tinykey
#ifndef __READ_ONCE
#define __READ_ONCE(x)	(*(const volatile __unqual_scalar_typeof(x) *)&(x))
#endif
tinykey
()
Ответ на: комментарий от dataman

Бл. Давай теперь банить всех явных профи тут. Их же избыток, да. Ты не прав. Или ты на девочек обижен. @maxcom FYI.

rumgot ★★★★★
()
Последнее исправление: rumgot (всего исправлений: 1)
Ответ на: комментарий от shdown

Что мешает использовать _atomic* и _sync*?

А зачем? У тебя есть фича в стандарте, которая запрещает компилятору делать оптимизации. Она подходит. Её взяли. То что POSIX там что-то свое про volatile думает, это его проблемы.

tinykey
()
Ответ на: комментарий от tinykey

В стандарте да. До стандарта все делали как умели

Я говорил про код, написанный в рамках C++17.

IFAIK, volatile для многопоточности бесполезен в современном C++ном мире.

eao197 ★★★★★
()
Ответ на: комментарий от tinykey

У тебя есть фича в стандарте, которая запрещает компилятору делать оптимизации

Нет. Это нигде не написано в стандарте.

Были случаи, когда

int mul2(volatile int *p)
{
    return *p + *p;
}
компилировалось в
    mov (%rdi),%eax
    mov (%rdi),%eax
    shl $1,%eax
    ret
Было два чтения? Получи два чтения. Больше ничего не гарантируется.

То что POSIX там что-то свое про volatile думает, это его проблемы.

А POSIX тут при чём? Есть стандарт Си. POSIX не может ему противоречить, если что. POSIX ничего дополнительно про volatile не говорит, ЕМНИП.

Сигналы, sig_atomic_t и гарантии на volatile sig_atomic_t есть в стандарте Си.

shdown
()
Последнее исправление: shdown (всего исправлений: 2)
Ответ на: комментарий от eao197

IFAIK, volatile для многопоточности бесполезен в современном C++ном мире.

Он бесполезен для многопоточности вообще везде. Нигде нет гарантии на его поведение в многопотоке. В ядре извращаются, насколько я понимаю, потому, что его разрабатывали ещё когда __atomic_*/__sync_* не было. А может и по сей день им нужно сохранять совместимость с древними версиями GCC.

shdown
()
Ответ на: комментарий от shdown

Он бесполезен для многопоточности вообще везде.

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

eao197 ★★★★★
()
Ответ на: комментарий от shdown

Нет. Это нигде не написано в стандарте.

Написано:

A volatile declaration can be used to describe an object corresponding to a memory-mapped input/output port or an object accessed by an asynchronously interrupting function. Actions on objects so declared are not allowed to be «optimized out» by an implementation or reordered except as permitted by the rules for evaluating expressions.

An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. Therefore any expression referring to such an object shall be evaluated strictly according to the rules of the abstract machine, as described in 5.1.2.3. Furthermore, at every sequence point the value last stored in the object shall agree with that prescribed by the abstract machine, except as modified by the unknown factors mentioned previously.165) What constitutes an access to an object that has volatile-qualified type is implementation-defined.

Прямые цитата из 9899:2023.

А POSIX тут при чём? Есть стандарт Си. POSIX не может ему противоречить, если что. POSIX ничего дополнительно про volatile не говорит, ЕМНИП.

А в стандарте C нет ничего про «это можно использовать только для сигналов».

tinykey
()
Ответ на: комментарий от shdown

Он бесполезен для многопоточности вообще везде. Нигде нет гарантии на его поведение в многопотоке. В ядре извращаются, насколько я понимаю, потому, что его разрабатывали ещё когда _atomic/_sync не было. А может и по сей день им нужно сохранять совместимость с древними версиями GCC.

Ну это до сих пор работает. Либо все системы на Linux сейчас внезапно перестанут работать и начнут проигрывать в гонки, либо ты неправ. Моя система все ещё работает.

tinykey
()
Ответ на: комментарий от shdown

А может и по сей день им нужно сохранять совместимость с древними версиями GCC

Нет, для последнего ядра требуется не меньше GCC 8.

tinykey
()
Ответ на: комментарий от shdown

P.S. Напомню как выглядит определение атомарных функций:

void atomic_store( volatile A* obj , C desired );
tinykey
()
Ответ на: комментарий от tinykey

6 The least requirements on a conforming implementation are:

Volatile accesses to objects are evaluated strictly according to the rules of the abstract machine.

(дальше не связанные с volatile вещи)

What constitutes an access to an object that has volatile-qualified type is implementation-defined.

Усё. Ваша первая цитата относится к информативной секции (объяснению причин, зачем нужен volatile), не формальной спецификации.

shdown
()
Последнее исправление: shdown (всего исправлений: 1)
Ответ на: комментарий от shdown

Усё. Ваша первая цитата относится к информативной секции (объяснению причин, зачем нужен volatile), не формальной спецификации.

Если ты отрицаешь что volatile работает, то ты так же отрицаешь и все семейство atomic_* функций, потому что там как раз volatile. Это очень странный спор.

tinykey
()
Ответ на: комментарий от shdown

Усё. Ваша первая цитата относится к информативной секции (объяснению причин, зачем нужен volatile), не формальной спецификации.

Первая цитата относится к секции 6 Language.

tinykey
()
Ответ на: комментарий от shdown

Он бесполезен для многопоточности вообще везде. Нигде нет гарантии на его поведение в многопотоке.

Реализация pthread в musl:

int pthread_spin_lock(pthread_spinlock_t *s)
{
	while (*(volatile int *)s || a_cas(s, 0, EBUSY)) a_spin();
	return 0;
}

А на musl половина контейнеров работает. Простите, но это какое-то безумие, отрицать один из базовых механизмов синхронизации в C.

tinykey
()
Ответ на: комментарий от shdown

Ну и вишенка на торте, собственно сам builtin из GCC:

/* x86-64 specific - MOV instruction with proper barriers */
static inline type atomic_load_x86(const type *ptr, int memorder)
{
    type result;
    
    if (memorder == __ATOMIC_SEQ_CST) {
        __asm__ __volatile__ ("mfence" ::: "memory");
    }
    
    __asm__ __volatile__ (
        "mov %1, %0"
        : "=r" (result)
        : "m" (*ptr)
        : "memory"
    );
    
    return result;
}
tinykey
()
Ответ на: комментарий от tinykey

А зачем? У тебя есть фича в стандарте, которая запрещает компилятору делать оптимизации.

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

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

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

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

tinykey
()
Ответ на: комментарий от tinykey

Это защитит от реордеринга и кеширования, что от volatile в этом контексте и требуется.

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

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

Ага. Я так понимаю, претензия @eao197 в том, что это не поможет синхронизировать доступ к буферу в целом.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

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

А больше здесь ничего и не нужно.

Ага. Я так понимаю, претензия @eao197 в том, что это не поможет синхронизировать доступ к буферу в целом.

С чего от это взял, если там mutex?

tinykey
()
Ответ на: комментарий от tinykey

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

А больше здесь ничего и не нужно.

Нет. Доступ из нескольких тредов тупо на volatile не синхронизируешь, нужно городить вокруг него. Собственно, что приведённые тобой кишки libc и делают.

С чего от это взял, если там mutex?

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

hateyoufeel ★★★★★
() автор топика
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от hateyoufeel

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

Все так. В тех методах, которые сравниваю head и tail, лок не нужен, потому что и head и tail – volatile. В тех методах, в которых происходит get и put лок есть.

tinykey
()
Ответ на: комментарий от tinykey

В тех методах, которые сравниваю head и tail, лок не нужен, потому что и head и tail – volatile.

Вот тут у тебя и будет гонка с другими тредами. Буфер может измениться между чтением head и tail.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Нет. Доступ из нескольких тредов тупо на volatile не синхронизируешь, нужно городить вокруг него. Собственно, что приведённые тобой кишки libc и делают.

Я не понимаю что здесь подразумевается под «синхронизируешь доступ». volatile решает одну единственную задачу – гарантирует, что ты каждый раз читаешь заново в том месте, где ты читать хотел. Все. Это буквально weak model, если мы говорим про атомики (именно такая модель в Linux).

tinykey
()
Ответ на: комментарий от hateyoufeel

Вот тут у тебя и будет гонка с другими тредами. Буфер может измениться между чтением head и tail.

Не будет, потому что эти методы вызываются под локом.

tinykey
()
Ответ на: комментарий от tinykey

P.S. А, ну и они приватные. У публичного лок есть:


template <class T>
bool ring_buffer<T>::_empty(void)
{
	return (_head == _tail) ? true : false;
}

template <class T>
bool ring_buffer<T>::_full(void)
{
	return (_head+1)%_size == _tail ? true : false;
}

template <class T>
bool ring_buffer<T>::empty(void)
{
	std::lock_guard<std::mutex> lock(_mtx_write);
	return (_head == _tail) ? true : false;
}
tinykey
()
Ответ на: комментарий от tinykey

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

tinykey
()
Ответ на: комментарий от tinykey

Не будет, потому что эти методы вызываются под локом.

Не все. clear, reset и конструкторы/операторы без мютекса идут.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Не все. clear, reset и конструкторы/операторы без мютекса идут.

Эти операции явно start / teardown и к логике работы отношения не имеют.

tinykey
()
Ответ на: комментарий от tinykey

Конструкторы копирования/перемещения к логике работы отношения не имеет? Там по всему получается, что ты не можешь скопировать уже инициализированный буфер, если он может где-то использоваться, потому что рискуешь скопировать залоченный мютекс, например. Либо скопировать его в невалидном состоянии. Либо ещё что.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

Конструкторы копирования/перемещения к логике работы отношения не имеет? Там по всему получается, что ты не можешь скопировать уже инициализированный буфер, если он может где-то использоваться, потому что рискуешь скопировать залоченный мютекс, например. Либо скопировать его в невалидном состоянии. Либо ещё что.

Я слабо себе представляю ситуацию в которой нужно копировать ринг буфер с которым прямо сейчас кто-то работает. Это тестовое, все-таки.

tinykey
()
Ответ на: комментарий от tinykey

Я слабо себе представляю ситуацию в которой нужно копировать ринг буфер с которым прямо сейчас кто-то работает. Это тестовое, все-таки.

А это не важно. Багиня себя позиционирует очень крутым разработчиком, который пишет софт без багов, и тут тебе вот такие вот детские косяки.

hateyoufeel ★★★★★
() автор топика
Ответ на: комментарий от hateyoufeel

А это не важно. Багиня себя позиционирует очень крутым разработчиком, который пишет софт без багов, и тут тебе вот такие вот детские косяки.

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

tinykey
()
Ответ на: комментарий от hateyoufeel

Я так понимаю, претензия @eao197 в том, что это не поможет синхронизировать доступ к буферу в целом.

Моя претензия в том, что volatile в C++ отношения к многопоточности не имеет. Хочешь гарантировано читать то, что записала другая нить – либо делай это под мутексом, либо через atomic-и.

Здесь мутексы есть, значит volatile нафиг не упал. А если кто-то пытается вставлять в C++ный код volatile для каких-то гарантий касательно многопоточности, тот ССЗБ.

eao197 ★★★★★
()
Ответ на: комментарий от tinykey

У меня лично есть стилистические вопросы

А ваш код где-нибудь можно увидеть?

eao197 ★★★★★
()
Ответ на: комментарий от tinykey

Это стилистическая мелочь, тут начали писать что там код не код и все плохо.

Тут бы цитату что именно так и начали писать.

Оказалось что обычно тестовое задание с какими-то допущениями.

И на тестовом задании к коду от гуру столько вопросов, что лучше и не начинать их задавать.

К тем, что уже было озвучено, а так же вот к этому вашему ыкспердному высказыванию «Я слабо себе представляю ситуацию в которой нужно копировать ринг буфер с которым прямо сейчас кто-то работает» еще один очевидный вопрос: а какого хера у thread safe ring buffer-а вообще есть конструктор/оператор копирования/перемещения? Какая религия запрещала их задизейблить?

Ну а если они таки есть, да еще и реализованы (причем оператор копирования еще и послал exception safety вдоль), то какого хера они не thread safe?

Причем она-то делала это все на C++17, в языке уже куча возможностей исправить все это, отсылки на старый стандарт не проканают.

В общем, я не удивлен увиденному. Ведь эта дама в своем резюме как-то указала отличное знание C++. После таких слов именно такого и остается ожидать. Не говоря уже про недавнюю сентенцию о том, что в многопоточном программировании все просто: Его крокейшество о вредности СУБД, если архитектурно она для программ, а не живого человека (комментарий)

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Моя претензия в том, что volatile в C++ отношения к многопоточности не имеет. Хочешь гарантировано читать то, что записала другая нить – либо делай это под мутексом, либо через atomic-и.

На чем строятся эти домыслы? Мы видим обратное в огромном количестве опенсурсных проектов.

tinykey
()
Ответ на: комментарий от tinykey

На чем строятся эти домыслы?

На том, что пишут люди поумнее и поопытнее меня в этих наших Интернетиках.

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Тогда все просто — собираемся и едем на следующий pwn2own, у нас десятки RCE в ядре и одном из самых популярных контейнеров.

tinykey
()
Ответ на: комментарий от eao197

Он имеет отношение не к многопоточности, а к асинхронному изменению значения переменной и/или наличию сайд эффектов при обращении к переменной (memory-mapped IO).

Многопоточность требует более жестких гарантий компилятора относительно работы памяти, чем просто асинхронность.

wandrien ★★★
()
Ответ на: комментарий от wandrien

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

tinykey
()
Ответ на: комментарий от wandrien

Он имеет отношение не к многопоточности, а к асинхронному изменению значения переменной и/или наличию сайд эффектов при обращении к переменной (memory-mapped IO).

Вы мне это зачем пишете?

eao197 ★★★★★
()
Ответ на: комментарий от tinykey

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

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

Зачем нужен volatile в многопоточности?

Так что завязывайте спорить с голосами в своей голове.

Во-вторых, про баги. Этих самых багов было указано целых два:

  1. Отсутствие thread-safety для конструкторов/операторов копирования/перемещения и публичных методов clear/reset. То, что лично вы в этом проблем не видите – это вопрос не к коду, а к вам. Вы, кстати, не ответили а где на ваш код можно посмотреть, чтобы понять уровень говорящего. По факту же имеем класс, который заявлен как thread safe и у которого, сюрпрайз-сюрпрайз, несколько публичных (т.е. доступных всем и каждому) методов не являются thread safe. Если это не баг то что?
  2. Оператор копирования реализован без оглядки на exception safety (upd: нет обеспечения strong exception safety). При этом в ключиках компиляции там -fexceptions явно указан. Вам нужно объяснить что такое exception safety?

Ну и еще одна штука, которая для обобщенного контейнера (которым и должен был бы являться класс ring_buffer) вполне может рассматриваться как баг: нет поддержки типов, которые не являются default constructible. Или вам этот момент тоже нужно объяснить?

eao197 ★★★★★
()
Последнее исправление: eao197 (всего исправлений: 2)
Ответ на: комментарий от eao197

Пару придирок.

std::unique_lock<std::mutex> lock(_mtx_read);
if(_full()) {
   res = _cond_read.wait_for(lock, std::chrono::nanoseconds(timeout));
}
if(res == std::cv_status::no_timeout && !_full()) {
   ...
   return true;
}
return false;

У неё wait_for без предиката. Из-за ложного просыпания get/put могут вернуть false, не подождав требуемого таймаута. Не то что бы ошибка, но нюанс.

if constexpr(std::is_scalar_v<T>) {
   memset(_buf, 0, _size*sizeof(T));
}

std::is_scalar_v<T> для enum вернёт true. Но в enum может не быть элемента с нулевым значением. Хорошо бы иметь возможность задать значение для инициализации.

ox55ff ★★★★★
()
Ответ на: комментарий от tinykey

Ты увидел знакомое слово и решил, что здесь используются volatile-переменные?

__asm__ это расширение GNU C, а __asm__ __volatile__ означает, что ассемблерный код может иметь side effects и его нельзя выпиливать.

Этот пример свидетельствует против твоего тезиса, потому что используется inline assembly, а не volatile-переменные.

shdown
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.