Changed some language in Writing Programs.
This commit is contained in:
parent
554d74b7f2
commit
7fbff1f6cc
1 changed files with 26 additions and 6 deletions
|
|
@ -688,7 +688,7 @@ Notice that @code{within} is defined with standard @code{defun} rather than @cod
|
||||||
|
|
||||||
Also notice how the first timeout behaves: when the 50 ms deadline expires before the card arrives, the @code{chan-recv} event is cleanly cancelled without consuming the message. The card remains on the channel, available to be received by the subsequent 500 ms wait.
|
Also notice how the first timeout behaves: when the 50 ms deadline expires before the card arrives, the @code{chan-recv} event is cleanly cancelled without consuming the message. The card remains on the channel, available to be received by the subsequent 500 ms wait.
|
||||||
|
|
||||||
An operation that is synced right where it is written does not need to be an event first. @code{(chan-put ch v)} does what @code{(sync (chan-send ch v))} does, and @code{(chan-get ch)} what @code{(sync (chan-recv ch))} does, in the way Guile's fibers have @code{put-message} beside @code{put-operation}. They wait, and are cancelled, exactly as @code{sync} is, but no event value is built, so they are cheaper. Use the event forms when you need an event: to @code{choose} between operations, to @code{wrap} one, or to hand one to a function such as @code{within}. Our players could just as well be written with @code{chan-put}:
|
An operation that is synced right where it is written does not need to be an event first. @code{(chan-put ch v)} does what @code{(sync (chan-send ch v))} does, and @code{(chan-get ch)} what @code{(sync (chan-recv ch))} does. They wait, and are cancelled, exactly as @code{sync} is, but no event value is built, so they are cheaper. Use the event forms when you need an event: to @code{choose} between operations, to @code{wrap} one, or to hand one to a function such as @code{within}. Our players could just as well be written with @code{chan-put}:
|
||||||
|
|
||||||
@codeblock[#:lang "bjolang"]|{
|
@codeblock[#:lang "bjolang"]|{
|
||||||
(defbjo (player hand seat)
|
(defbjo (player hand seat)
|
||||||
|
|
@ -791,7 +791,7 @@ Conversely, you may occasionally need to wait on a channel or event from inside
|
||||||
|
|
||||||
@subsubsection{Compared with Go}
|
@subsubsection{Compared with Go}
|
||||||
|
|
||||||
Go is the obvious point of comparison: goroutines and unbuffered channels are the same model of fibers meeting at a rendezvous. The repository has the same benchmarks written in both languages (@code{bench/bjolang/cmlbench.bjo} and @code{go run . -suite cmlbench} in @code{bench/go}). Every Bjolang channel operation races the cancellation token of its scope, so Go is measured twice where that matters: as plain channel operations, and with each one selecting against @code{ctx.Done()}, which is what a Go program that can be stopped has to write. Bjolang is measured twice as well: with events and @code{sync}, and with @code{chan-put} and @code{chan-get}, so the difference between the two columns is what building an event and syncing it costs. The best of five runs, in nanoseconds per operation, on a Ryzen 9 5900X under Linux with .NET 10 and Go 1.25:
|
Go is the obvious point of comparison: goroutines and unbuffered channels are the same model of fibers meeting at a rendezvous. The main repository has the same benchmarks written in both languages (@code{bench/bjolang/cmlbench.bjo} and @code{go run . -suite cmlbench} in @code{bench/go}). Every Bjolang channel operation races the cancellation token of its scope, so Go is measured twice: as plain channel operations, and with each one selecting against @code{ctx.Done()}, which is what a go program has to stop execution. Bjolang is measured twice as well: with events and @code{sync}, and with @code{chan-put} and @code{chan-get}, so the difference between the two bjolang columns is the cost of what building an event and syncing it costs. The best of five runs, in nanoseconds per operation, on a Ryzen 9 5900X under Linux with .NET 10 and Go 1.25:
|
||||||
|
|
||||||
@read-table{
|
@read-table{
|
||||||
| Benchmark | Bjolang, @code{sync} | Bjolang, @code{chan-put} | Go | Go, with @code{ctx.Done()} |
|
| Benchmark | Bjolang, @code{sync} | Bjolang, @code{chan-put} | Go | Go, with @code{ctx.Done()} |
|
||||||
|
|
@ -802,17 +802,17 @@ Go is the obvious point of comparison: goroutines and unbuffered channels are th
|
||||||
| An 8-way choose where one branch fires | 152 | 153 | 400 | 443 |
|
| An 8-way choose where one branch fires | 152 | 153 | 400 | 443 |
|
||||||
}
|
}
|
||||||
|
|
||||||
In the choose row only the sender can use @code{chan-put}, since a choice between operations is what an event is for, and the time is set by the receiving @code{choose}. On the ring, syncing an event costs about a fifth of each message.
|
In the choose row only the sender can use @code{chan-put}, since the receiving side has to use channel-receive (and thus allocates). On the ring, syncing an event costs about a fifth of each message. For some reason, thi ring inside a nested scope is faster in bjolang, and I haven't figured out why yet.
|
||||||
|
|
||||||
These are small benchmarks on one machine, and they measure the cost of meeting at a channel, not of the work between meetings. Go is ahead in other ways: a Go channel operation never allocates, where a Bjolang send synced as an event allocates 32 bytes and a @code{choose} 40 (@code{chan-put} and @code{chan-get} allocate nothing); any Go function can block without being written as a bjoroutine; and the Go scheduler preempts a goroutine that computes for too long, where a fiber has to be moved off the pool with @code{spawn/thread}.
|
These are small benchmarks on one machine, and they only measure the cost of the concurrency primitives, not of the work between rendevouz. Go is ahead in other ways: a Go channel operation never allocates, where a Bjolang send synced as an event allocates 32 bytes and a @code{choose} 40 (@code{chan-put} and @code{chan-get} allocate nothing). Any Go function can block without being written as a bjoroutine and the Go scheduler preempts a goroutine that computes for too long, where a fiber has to be moved off the pool with @code{spawn/thread}.
|
||||||
|
|
||||||
What Bjolang offers that Go does not is in the language rather than in the numbers. A Go @code{select} is a statement: its cases are fixed in the source, and a channel operation cannot be passed to a function, wrapped, or combined with others. In Bjolang an event is a value, which is what made @code{within} above a two-line function that works on any event, and @code{choose}, @code{wrap}, @code{guard} and @code{with-nack} build new events out of old ones. Go has no structured concurrency either. Waiting for goroutines, stopping them, and reporting their failures are a @code{sync.WaitGroup}, a @code{context.Context} passed to every function and checked at every channel operation, and an @code{errgroup}, written by hand each time. A goroutine that nothing waits for leaks without a sound. In Bjolang a scope does all of that by construction: it waits for its fibers, raises their failures when it ends, releases the files they opened, and cancels them through a token that every @code{sync} watches without being asked.
|
What Bjolang offers that Go does not is in the CML vocabulary rather than in the numbers. A Go @code{select} is a statement, its cases are fixed in the source, and a channel operation cannot be passed to a function, wrapped, or combined with others. In Bjolang an event is a value, which is what made @code{within} above a two-line function that works on any event, and @code{choose}, @code{wrap}, @code{guard} and @code{with-nack} build new events out of old ones. Go has no structured concurrency either. Waiting for goroutines, stopping them, and reporting their failures are a @code{sync.WaitGroup}, a @code{context.Context} passed to every function and checked at every channel operation, and an @code{errgroup}, written by hand each time. A goroutine that nothing waits for leaks without a sound. In Bjolang a scope does all of that by construction: it waits for its fibers, raises their failures when it ends, releases the files they opened, and cancels them through a token that every @code{sync} watches without being asked.
|
||||||
|
|
||||||
@subsection{Effects}
|
@subsection{Effects}
|
||||||
|
|
||||||
Our deck is shuffled using @code{shuffle-vec}, which is non-deterministic. While randomness is ideal during gameplay, it makes automated tests brittle when verifying deterministic outcomes like trick winners. The traditional object-oriented or functional workaround is dependency injection—passing an explicit shuffle function down through every layer of the call stack.
|
Our deck is shuffled using @code{shuffle-vec}, which is non-deterministic. While randomness is ideal during gameplay, it makes automated tests brittle when verifying deterministic outcomes like trick winners. The traditional object-oriented or functional workaround is dependency injection—passing an explicit shuffle function down through every layer of the call stack.
|
||||||
|
|
||||||
Bjolang provides an alternative mechanism: *algebraic effects*. With effects, a function simply declares and performs an abstract operation, allowing an enclosing caller higher up the call stack to dynamically decide how that operation is handled.
|
Bjolang provides an alternative mechanism: *tail-resumptive algebraic effects*. With effects, a function simply declares and performs an abstract operation, allowing an enclosing caller higher up the call stack to dynamically decide how that operation is handled.
|
||||||
|
|
||||||
@subsubsection{Declaring an effect}
|
@subsubsection{Declaring an effect}
|
||||||
|
|
||||||
|
|
@ -1365,3 +1365,23 @@ bjo> :show random-u
|
||||||
Import modifiers apply as they would in an entry: after @code{(import (prefix (std random) "r/"))} the name to ask about is @code{r/random-bool}, and @code{:show} says that it is @code{random-bool} from @code{(std random)}. A name with no documentation is shown with where it comes from and "with no doc".
|
Import modifiers apply as they would in an entry: after @code{(import (prefix (std random) "r/"))} the name to ask about is @code{r/random-bool}, and @code{:show} says that it is @code{random-bool} from @code{(std random)}. A name with no documentation is shown with where it comes from and "with no doc".
|
||||||
|
|
||||||
Type @code{:help} to view available REPL commands, or press @code{Ctrl-D} (or type @code{:quit}) to exit.
|
Type @code{:help} to view available REPL commands, or press @code{Ctrl-D} (or type @code{:quit}) to exit.
|
||||||
|
|
||||||
|
@subsection{Performance}
|
||||||
|
Bjolang is compiled into pretty straight-forward c#, but without spans or ref structs. It performs pretty similar to c#, except for some small corners of the language that are different.
|
||||||
|
|
||||||
|
@subsubsection{Strings}
|
||||||
|
Strings in bjolang are utf8, as such they do @em{not} support random access. This means we navigate them differently, using string-cursors, which is explained in the part about strings. Navigating a string using cursors is slower than blasting through integer indices, and if your code needs to be really fast, there are some things you can think about:
|
||||||
|
|
||||||
|
@read-list{
|
||||||
|
- Use the provided search functions and the loop form. They work on raw memory, and can thus work around some of the limits that the safety of string-cursor give.
|
||||||
|
- use @code{string-cursor-next+ref} instead of next and then ref.
|
||||||
|
- some of the higher order functions are also faster, depending on use case. Benchmark and see.
|
||||||
|
- Use slices when copying is the bottleneck: an operation ending with @code{/slice} will return a view into the string instead of copying. This will retain the whole string in memory, so use it wisely.
|
||||||
|
}
|
||||||
|
|
||||||
|
@subsubsection{loops}
|
||||||
|
Most of the time you should use the @code{loop} form. It will be the fastest, and is usually flexible enough to do what you need. The only case is for hot loops (ones that are run often) that are performance sensitive. For those, using the various @code{-fold}, @code{-for-each}, and @code{-iter} functions will be faster for the collections that are tree-based. Those collections are: @code{Vec}, @code{Map}, @code{HashSet}, @code{OrderedMap}, and @code{OrderedMap}. The overhead of loop vs. fold is often something like 3x, with fold being as fast or slightly faster thana @code{foreach} on a C# @code{List<T>}. It does not, however, beat using indexed iteration over the @code{List<T>} due to the @code{foreach} doing some safety checks to detect concurrent mutation.
|
||||||
|
|
||||||
|
The absolutely fastest collection to loop over is, unsurprisingly, the Array. In bjolang it should be just as fast as in c# (again, without spans).
|
||||||
|
|
||||||
|
The loop form gets turned into a kind of goto soup. For the single level case, it is just as efficient as a c# @code{while} loop, but for really complex cases with many levels and subloops with clauses the JIT in dotnet may struggle. In that case you either live with the clarity and simplicity it gives, or you reach for the glorified gotos of names lets. This is probably not going to be a problem though. I tried to rewrite the worst loop form in bjolang (a 16 line monstrosity with one large multi-level claused subloop and a regular subloop) and there was no difference.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue