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 взаимодействует с многопоточностью (C11 знает про потоки)? Хоть какая-то его семантика? Это relaxed, acquire/release, seq_cst?

Нигде. Там написано, что для memory-mapped I/O и ловли сигналов его можно использовать. Ты сам процитировал эти слова.

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

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

Да-да. «У дидов всё работало, а тут понаписали каких-то стандартов.» «Всю жизнь так делали, а тут — говорят, нельзя.» Очень много раз мы такое слышали. Так же на strict aliasing ругаются, на UB при переполнениях знаковых и прочее.

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

И чо? Формально так нельзя. Мы говорим про формальности.

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

https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf

35 The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior

Стандарт считает, что volatile — это не atomic, и операции над volatile не atomic. Ты с этим будешь спорить?

Вот, например, слова, которые показывают, что стандарт не считает accessing a volatile object == atomic operations:

An iteration statement may be assumed by the implementation to terminate if its controlling expression is not a constant expression,186) and none of the following operations are performed in its body, controlling expression or (in the case of a for statement) its expression-3: 187)

— input/output operations

— accessing a volatile object

— synchronization or atomic operations.

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

И вот ещё:

https://gcc.gnu.org/onlinedocs/gcc-4.3.2/gcc/Qualifiers-implementation.html

          volatile int *dst = somevalue;
          volatile int *src = someothervalue;
          *dst = *src;

will cause a read of the volatile object pointed to by src and store the value into the volatile object pointed to by dst. There is no guarantee that these reads and writes are atomic, especially for objects larger than int.

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

Впрочем, говорить тут что-то излишне, давайте покажу:

enum { BUF_SIZE = 1024 };
char buffer[BUF_SIZE];

volatile int buffer_ready = 0;

void buffer_init(void)
{
    for (int i = 0; i < BUF_SIZE; ++i) {
        buffer[i] = 0;
    }
    buffer_ready = 1;
}

Компилируем. gcc version 15.2.0 (Debian 15.2.0-4).

$ gcc -Wall -Wextra -O3 -c foo.c
$ objdump -S foo.o

foo.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <buffer_init>:
   0:	b9 80 00 00 00       	mov    $0x80,%ecx
   5:	31 c0                	xor    %eax,%eax
   7:	48 8d 3d 00 00 00 00 	lea    0x0(%rip),%rdi        # e <buffer_init+0xe>
   e:	c7 05 00 00 00 00 01 	movl   $0x1,0x0(%rip)        # 18 <buffer_init+0x18>
  15:	00 00 00 
  18:	f3 48 ab             	rep stos %rax,(%rdi)
  1b:	c3                   	ret

Компилятор поместил buffer_ready = 1 (movl $0x1,…) до цикла for (rep stos …).

(За идею спасибо этой статье: https://users.cs.utah.edu/~regehr/papers/emsoft08-preprint.pdf)

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

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

То же самое значит volatile в C. Кажется этот разговор пошел по кругу, заключение я повторю: если ты прав, любая Linux система полна data races из-за того что на семантике volatile там построены буквально все синхронизации, включая спинлоки и мьютексы. Если это так, ты можешь ехать на pwn2own и забрать там все призы, обеспечив себе и своим потомкам сытую старость. Если мы этого не увидим, значит семантика volatile работает и ты был неправ.

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

если ты прав, любая Linux система полна data races из-за того что на семантике volatile там построены буквально все синхронизации

У вас какой-то собственный контекст разговора.

В ядре Linux ядреная системщина и привязка к конкретной аппаратуре. И, понятное дело, на таком уровне можно вручную порасставлять барьеры памяти для того, чтобы видеть нужные изменения внесенные другим тредом на другом ядре, а volatile в код добавляется для того, чтобы шибко умный компилятор оптимизатором типа «лишние» инструкции не выбрасывал.

Обсуждение же volatile применительно к многопоточности началось с C++ного кода, который к железу и ядру Linux-а не имеет отношения. От слова совсем.

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

Странный критерий поиска истины. Пихать в такой код volatile просто из-за того, что в Linux-е «на семантике volatile там построены буквально все синхронизации» (с) – ну странно, по меньей мере.

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

Обсуждение же volatile применительно к многопоточности началось с C++ного кода, который к железу и ядру Linux-а не имеет отношения. От слова совсем.

А какая разница-то, компилятор общий?

Странный критерий поиска истины. Пихать в такой код volatile просто из-за того, что в Linux-е «на семантике volatile там построены буквально все синхронизации» (с) – ну странно, по меньей мере.

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

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

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

Если ты программируешь, условно, под RSX-11 на PDP-11, где обработка событий от системы реализована как вызов асинхронных обработчиков в контексте единственного потока задачи – то volatile твой друг.

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

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

А какая разница-то, компилятор общий?

Код к ядру Linux-а не относится. И не прибит гвоздями ни к компилятору (как Linux), ни к платформе.

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

Так у тебя тут запись в buffer не имеет сторонних эффектов. Разумеется компилятор может переместить код без сторонних эффектов как ему хочется.

Если сделать volatile char buffer[BUF_SIZE], то код будет совсем другой.

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

volatile гарантирует только последовательность обращений к объекту в машинном коде, но не гарантирует ничего насчёт того, в каком порядке и как именно изменения значений в памяти видят разные потоки, и атомарность изменений тоже не гарантирует.

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

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

Толк от него есть: помешать компилятору закешировать значение. Ну вот же, прямо в C23 API:

void atomic_store( volatile A* obj , C desired );

Ты думаешь они квалифицировали потому что им было скучно и захотелось? :)

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

Код к ядру Linux-а не относится. И не прибит гвоздями ни к компилятору (как Linux), ни к платформе.

А есть платформы или компиляторы где компилятор может взять и закешировать переменную, квалифицированную как volatile? Ты можешь такие назвать?

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

А есть платформы или компиляторы где компилятор может взять и закешировать переменную, квалифицированную как volatile?

Я вообще не о том.

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

Может подскажите, почему вообще можно предположить, что вы что-то понимаете в программировании?

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

жаль она больше не может поучаствовать в обсуждении

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

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

Толк от него есть: помешать компилятору закешировать значение.

А как это к мультипроцессорности относится-то? Компилятор – это отдельное устройство у тебя, что ли?

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

Если сделать volatile char buffer[BUF_SIZE], то код будет совсем другой.

так там тогда жесть вообще с этим буфером будет, он побайтно в цикле его заполнять станет
и всё равно конпелятор не гарантирует тут последовательности исполнения, buffer и buffer_ready никак не связаны

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

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

Ну так нехер его побайтно в цикле заполнять. Используй memset().

и всё равно конпелятор не гарантирует тут последовательности исполнения, buffer и buffer_ready никак не связаны

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

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

и всё равно конпелятор не гарантирует тут последовательности исполнения

Это откуда такое утверждение такое взялось?

buffer и buffer_ready никак не связаны

Они связаны ключевым словом volatile.

Предлагаю задуматься, что произойдёт в коде, где объявлены:

volatile char control_port;
volatile char data_port;

если компилятор «не гарантирует последовательности исполнения».

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

Ну так нехер его побайтно в цикле заполнять. Используй memset().

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

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

если так, то ок, лень искать

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

у нас ведь buffer_ready для того и есть, что сам буфер снаружи заведомо ничем не меняется

Поэтому нужно так:

#include <stdatomic.h>
enum { BUF_SIZE = 1024 };
//volatile char buffer[BUF_SIZE];
//volatile int buffer_ready = 0;
char buffer[BUF_SIZE];
atomic_int buffer_ready = 0;

void buffer_init(void)
{
    for (int i = 0; i < BUF_SIZE; ++i) {
        buffer[i] = 0;
    }
    //buffer_ready = 1;
    atomic_store_explicit(&buffer_ready, 1, memory_order_release);
}
wandrien ★★★
()
Ответ на: комментарий от madcore

Вообще если заглянуть в результирующий код, то для x86 вся «магия» тут ограничивается чисто приказом компилятору не переупорядочивать сохранения за барьер.

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

Зачастую «сломанный» код просто молча работает на x86, потому что компилятор по случаю ничего не переупорядочивал, а железо x86 и так гарантировало порядок. Но при переносе на ARM сломанный код ломается.

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

Запись и чтение в u32 само по себе атомарно везде кроме alpha и arm64. Тебе не надо ничего для этого делать. Есть две проблемы: компилятор может кешировать, предыдущие операции могут не успеть закончиться. Первая проблема решается volatile, вторая барьером. Я не спорю с тем что стандартные атомики использовать разумнее, я спорю с отрицанием volatile.

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

Запись и чтение в u32 само по себе атомарно везде кроме alpha и arm64.

Ага. На первой по популярности платформе (arm64) оно не атомарно, значит можно не париться. Классная идея!

я спорю с отрицанием volatile.

Никто тут не отрицает volatile. Тут тебе пишут о том, что volatile нужен для другого. В многопоточном коде он не подходит.

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

Никто тут не отрицает volatile. Тут тебе пишут о том, что volatile нужен для другого. В многопоточном коде он не подходит.

Начинай сначала… он в определении атомарных функций. Прямо в стандарте. Хотите отрицать стандарт — бога ради.

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

То же самое значит volatile в C

facepalm.jpg. Может и asm inline означает то же самое, что inline?

Кажется этот разговор пошел по кругу, заключение я повторю

Опровергай мои аргументы и цитаты стандартов и документации компиятора. Или обтекай.

UPD: Впрочем, твой уровень уже понятен по «asm volatile — то же самое, что volatile-переменные».

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

Начинай сначала… он в определении атомарных функций. Прямо в стандарте. Хотите отрицать стандарт — бога ради.

Прямо в стандарте нет определения атомарных функций. Там есть только объявления.

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

Прямо в стандарте нет определения атомарных функций. Там есть только объявления.

Как-то странно про стандарт говорить «объявлена», но бог с ним. Там указатель на volatile объект.

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

Начинай сначала… он в определении атомарных функций. Прямо в стандарте.

Прекрасно, но где в коде Iron_Bug использование этих самых атомарных функций?

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

Мы по кругу ходим.

Вы – может быть.

Я как задал вопрос зачем volatile в многпоточности в C++ном коде, так ответа на него и не получил.

Зато вы тут разошлись в демонстрации хз чего хз зачем.

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

Здесь уже раз десять написано.

Если вами, то вы о C++ ничего не писали. Вы все про чистую Си-шечку и volatile в ядре Linux-а.

Про C++ ни слова. Хотя речь изначально шла о C++.

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

Как-то странно про стандарт говорить «объявлена», но бог с ним. Там указатель на volatile объект.

Я о том, что в стандарте нет тела функции. То, что в параметрах volatile, это ещё ничего само по себе не значит. Про барьеры и прочее тебе уже написали.

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

Я о том, что в стандарте нет тела функции. То, что в параметрах volatile, это ещё ничего само по себе не значит.

Очевидно, значит – теперь обращение к этой переменной будет трактоваться как обращение к volatile. Ровно этот же трюк делают люди в ядре, что позволяет читать/писать атомарно в неквалифицированные переменные:

int a;

WRITE_ONCE(a, 1);

и

int a;

atomic_store(&a, 1);

и

volatile int a;

a = 1;

будет равнозначно. Это запись значения в переменную с relaxed order, которая гарантированно не будет выкинута компилятором. Дальше начинаются истории про барьеры (для этого есть отдельные функции или методы), но в случае безумной женщины это все смысла не имеет, у неё там mutex стоит, который вырождает в полный memory barrier.

Про барьеры и прочее тебе уже написали.

Про барьеры и прочее я и сам писал.

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

но в случае безумной женщины это все смысла не имеет, у неё там mutex стоит, который вырождает в полный memory barrier.

Да уж.

Сперва происходит такой диалог:

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

tinykey: Атомики.

Чтобы затем тот же самый tinykey сделал вывод «это все смысла не имеет»

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

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

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

Вопросы «зачем нужен volatile в многопоточности» и «зачем нужен volatile в коде безумной женщины» – разные вопросы. Я отвечал на первый.

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

Я отвечал на первый.

Проблема в том, что вы начали отвечать на вопрос «зачем volatile в многопоточности на чистом Си». Только вот обсуждался C++ный код. В котором volatile для многопоточности не нужен. По крайней мере если вы не ядро на С++ пишете под конкретную железяку и не можете задействовать C++ный stdlib даже в части <atomic>.

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

atomic_store гарантирует барьер, а присваивание значения volatile переменной ничего не гарантирует

Да, ты прав, atomic_store по умолчанию sequential. Мне казалось relaxed.

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

В котором volatile для многопоточности не нужен.

Нужен, если мы говорим о случаях вроде shared memory, где компилятор будет уверен, что никто переменную менять не может и её можно кешировать.

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

Нужен, если мы говорим о случаях вроде shared memory, где компилятор будет уверен, что никто переменную менять не может и её можно кешировать.

Какую-такую переменную?

Если она в C++ атомарная (т.е. std::atomic<T>), то никакой volatile для нее не нужен.

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

Ну либо приведите пример кода, который бы подтверждал вашу мысль. Пример C++ного кода.

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

Если она в C++ атомарная (т.е. std::atomic), то никакой volatile для нее не нужен.

Нужен, поэтому у std::atomic есть методы для случая volatile atomic <T>:

void store (T val, memory_order sync = memory_order_seq_cst) volatile noexcept
tinykey
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.