Имбирная Ведьмочка
> и кроме крестов он так больше нигде и не работает
Почему это?
function foo(condition0, condition1){ switch( condition0){ case true: let some_non_trivial = 42 if( condition1){ console.log( "finally do something " + some_non_trivial) break } default: console.log( "do somethig else") } }
Имбирная Ведьмочка
> Кстати, самый очевидный вариант, как обычно, приходит на следующий день:
Мне кажется, что тут идеальна была бы конструкция типа однократного цикла:
block { if(condition0 ) { // some non-trivial code with variables declaration here // ... if( condition1 ) { // finally do something break; // to the end of the block } } // do somethig else }
Здесь block {...} — это эквивалент do {...} while(false);
Еще разрешить многократный break для выхода из нескольких вложенных циклов/блоков и можно будет достаточно сложные графы исполнения реализовывать.
}:+()___ [Smile]
> Еще разрешить многократный break для выхода из нескольких вложенных
> циклов/блоков и можно будет достаточно сложные графы исполнения реализовывать.
В очередной раз скажу, что я строго против многократных брейков в пользу нормального гоуту, ну или хотя бы "у нас есть гоуту дома" когда гоуту дома это брейк с пометкой.
Это всё равно, что нормальные именованные локальные переменные променять на смещения по стеку, причём стек постоянно перемещается между скоупами, так что одна и та же переменная в разных местах оказывается stack 1, stack 2, stack 4 и stack 7.
}:+()___ [Smile]
> Мне кажется, что тут идеальна была бы конструкция типа однократного цикла:
А если этой конструкции дать свой лейбл — останется буквально миллиметр до того, чтобы вырефакторить её в отдельную функцию с обычным ретурном.
Имбирная Ведьмочка
> В очередной раз скажу, что я строго против многократных брейков в пользу нормального гоуту
У кого по пять вложенных циклов, тот ССЗБ, а у меня обычно два вложения, на крайняк, три, городить ради этого всякие метки — это лишнее усложнение. Тогда уж сразу любой break/continue с меткой надо, чего мелочиться.
> А если этой конструкции дать свой лейбл — останется буквально миллиметр до того, чтобы вырефакторить её в отдельную функцию с обычным ретурном.
Если там огромный контекст протягивается снаружи внутрь, то далеко не миллиметр.
Хех. В Rust ведь блоки это выражения, так что можно так:
fn bar() -> i32 { 42 } fn foo(condition : bool) { if condition && { let v = bar(); v > 24 } { println!("alternative0"); } else { println!("alternative1"); } } pub fn main() { foo(true); foo(false); }
Хотя это не совсем эквивалентно тому, что я хочу. Переменные из блока внутри условия не доступны внутри блока if.
}:+()___ [Smile]
> Если там огромный контекст протягивается снаружи внутрь, то далеко не
> миллиметр.
Заверни в объект.
Ну или, как я уже сказал, если функция прямо упирается ногами и не выносится — я считаю, что простой goto after будет самым понятным решением, в силу своей простоты и очевидности:
if(condition0 ) { // some non-trivial code with variables declaration here // ... if( condition1 ) { // finally do something goto something_done; } } // do somethig else something_done:
...Что-что говорите, язык не поддерживает гоуту? Ну значит, язык — говно, и остаётся только страдать. 🗿
Начал эксперименты с llvm-ными корутинами.
Вот такой код:
fn generator Foo( u32 count ) : u32 { for( var u32 mut x= 0u; x < count; ++x ) { yield x; } } fn Main( u32 count ) { auto gen= Foo(count); while( true ) { if_coro_advance( x : gen ) { PrintInt(x); continue; } break; } } fn PrintInt( u32 x );
С оптимизациями компилируется в такое:
define void @_Z4Mainj(i32 %_arg_count) unnamed_addr #2 comdat { coro_not_done.lr.ph: %.not.i = icmp eq i32 %_arg_count, 0 br i1 %.not.i, label %._crit_edge, label %coro_not_donethread-pre-split coro_not_donethread-pre-split: ; preds = %coro_not_done.lr.ph, %coro_not_donethread-pre-split %0 = phi i32 [ %1, %coro_not_donethread-pre-split ], [ 0, %coro_not_done.lr.ph ] tail call void @_Z8PrintIntj(i32 %0) %1 = add nuw i32 %0, 1 %exitcond.not.i = icmp eq i32 %1, %_arg_count br i1 %exitcond.not.i, label %._crit_edge, label %coro_not_donethread-pre-split ._crit_edge: ; preds = %coro_not_donethread-pre-split, %coro_not_done.lr.ph ret void }
Изначально то фронтенд компилятора вставляет malloc/free для кадра стека генератора. Но оптимизатор всё заинлайнил и успешно убрал аллокации из кучи.
Но вот если сделать вложенные генераторы:
fn generator CountNumbers( u32 count ) : u32 { for( var u32 mut x= 0u; x < count; ++x ) { yield x; } } fn generator CountSquareNumbers( u32 count ) : u32 { auto gen= CountNumbers(count); while( true ) { if_coro_advance( x : gen ) { yield x * x; continue; } break; } }
Оптимизатор уже не справляется и вставляет пару вызовов malloc - для обоих генераторов. Но, если програть оптимизатор второй раз, то таки убираются аллокации из кучи. Хотя странно, велт даже однократный O3 не может убрать их.
Залез внутрь проходов llvm по подготовке корутин, обнаружил, что я делаю не совсем верно.
Теперь вроде malloc-и уходят при большой вложенности корутин.
malloc-ов нету и это хорошо. Плохо то, что какие-то мусорные undef значения и ветвления всё-же остаются.
Чё-то меня оптимизатор llvm вымораживает своей беспомощностью.
Должен ведь развернуться по-простому.
Ума не приложу, почему тут оптимизатор оставил уродский switch с phi-узлом на десять вариантов. И против такого даже O3 и второй прогон оптимизации не помогают.
Panzerschrek[CN]
> Не понятно, при этом, как реализовать взаимодействие с системными асинхронными API. Есть несколько вариантов.
Можно поизучать, как в Rust-овской библиотеке "tokio" это дело сделано.
Есть блог: https://tokio.rs/blog, там есть кое-что интересное.
Panzerschrek[CN]
> Второй вариант - в месте вызова асинхронного API вставлять цикл из опроса системного API на предмет готовности запроса, и если тот ещё не готов, переключать асинхронную функцию.
А вот видимо чтобы в холостую не молоть циклы CPU, в Rust внедрён механизм wake. Насколько я понимаю, с ним возможно через какую-нибудь системную очередь сообщений узнавать, какие операции ввода/вывода продвинулись и тем самым помечать соотвествующие async функции, вызов системных API в которых продвинулся.
A Waker is a handle for waking up a task by notifying its executor that it is ready to be run.
This handle encapsulates a RawWaker instance, which defines the executor-specific wakeup behavior.
The typical life of a Waker is that it is constructed by an executor, wrapped in a Context, then passed to Future::poll(). Then, if the future chooses to return Poll::Pending, it must also store the waker somehow and call Waker::wake() when the future should be polled again.
Я так понимаю, это абстракция над condition variable?
Имбирная Ведьмочка
> Я так понимаю, это абстракция над condition variable?
Может даже тупо atomic.
Только мне не понятно, зачем в Rust для этого сделали отдельный контекст, который скрыто передаётся в каждую async функцию. Не проще ли было в какою-нибудь переменную в thread local storage его положить?
Интересную штуку в Rust обнаружил.
async fn fib(x : u32) -> u32 { if x <= 1 { return 1; } fib( x - 1).await + fib(x - 2).await }
Не компилируется:
1 | async fn fib(x : u32) -> u32 | ^^^ recursive `async fn` | = note: a recursive `async fn` must be rewritten to return a boxed `dyn Future` = note: consider using the `async_recursion` crate: https://crates.io/crates/async_recursion
Всё дело в том, как работает в Rust async. Там прямо на уровне фронтенда компилятора под каждую async функцию создаётся уникальный структурный тип, поля которого - это локальные переменные async функции, которые нужно сохранить между await точками. При этом если одна async функция зовёт другую, то структура этой другой функции будет вложенной в структуру первой функции. Из-за этого вот так вот просто невозможно создать рекурсивную async функцию.
В Ü же я планирую сделать иначе. Память под объект async функции выделяется из кучи, а это значит, что с рекурсией проблем нету. Да, при этом есть переголова с выделением памяти из кучи, но она во многом устраняется проходами оптимизации. Есть специальных проход для корутин, который как раз нацелен на устранение излишних аллокаций из кучи. Судя по моим тестам оптимизатор умеет устранять аллокации во вложенных вызовах корутин, что в итоге в сравнении с Rust даст такую же производительность.
Тема в архиве.