From 21d0c9de06a0c9767fceacab8375c9910d35a43f253840fa52723c3d33f495ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linus=20Bj=C3=B6rnstam?= Date: Tue, 20 Oct 2026 08:01:41 +0200 Subject: [PATCH] aot clarifications --- _02_.Writing-programs.sz | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/_02_.Writing-programs.sz b/_02_.Writing-programs.sz index d6d4bd9..0fb0aea 100644 --- a/_02_.Writing-programs.sz +++ b/_02_.Writing-programs.sz @@ -1,4 +1,3 @@ - @section{Writing programs} While the first part focused on the core language constructs in single files, this part covers the wider ecosystem: breaking programs into modules, managing project dependencies, handling I/O, concurrency with bjoroutines, .NET interop, macros, and testing. @@ -393,8 +392,8 @@ The executable is beside the source, with no extension: @code{src/main}, or @cod - @strong{@code{bjo run} starts the native executable, and @code{bjo publish} copies it} into the output directory. The ordinary @code{.exe} is still built, for the debugger and for @code{-d}. - @strong{The build takes longer}, about 10 s more, and needs clang or gcc. The first build downloads .NET's ILCompiler package from NuGet. - @strong{The runtime settings are built into the executable.} The GC settings, such as @code{(runtime (gc-server #t))}, take effect. The settings of tiered compilation do nothing, because there is no JIT. - - @strong{A long-running program can be slower.} The JIT optimizes the code with what it sees the program do (dynamic PGO), and a native executable can not. A program that calls through many traits in a hot loop gains the most from that. Measured on one such program: 0.73 s on .NET and 0.89 s native. - - @strong{Reflection that is resolved at run time may fail.} Native AOT keeps only the code that it can see the program use. + - @strong{A long-running program can be slower.} The JIT optimizes the code with what it sees the program do (dynamic PGO), and a native executable can not. This matters for programs where the call graph stops the monomorphization or when trait calls cannot be determined at compile time. + - @strong{Reflection that is resolved at run time may fail.} Native AOT keeps only the code that it can see the program use. Bjolang does not use reflection by default, so this should be a small problem. } By default the executable runs on any x86-64 CPU, so it uses only the instructions that all of them have: no AVX, and 128-bit vectors. @code{aot-cpu} names a CPU to build for instead: @@ -2080,6 +2079,8 @@ Strings in bjolang are utf8, as such they do @em{not} support random access. Thi - 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. } +When parsing strings with a known format, (std scan) will be a lot faster than (std rx) and the good old string-split. Often by an order of magnitude. The only cost is IL code size, since it builds the parser in the code, which will have other costs, such as no inlining. + @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}. It does not, however, beat using indexed iteration over the @code{List} due to the @code{foreach} doing some safety checks to detect concurrent mutation. @@ -2138,7 +2139,7 @@ Use a @code{Map} to look up, update, insert and remove by key. It wins at all of Use an @code{OrderedMap} when the order of the keys matters: the smallest or largest key, the entries between two keys, or the entries in key order. A map can only answer those by reading every entry. An ordered map is also faster to iterate in full, so a program that walks a large map more often than it looks things up in it may prefer one. -@subsubsection{Vectors} +@subsubsection{Simd vectors} A CPU can do one operation on several numbers at once: 4 doubles, 8 ints or 32 bytes in one 256-bit register with AVX2. Bjolang does not do this for you, because the .NET JIT does not turn a loop into vector code. Two modules do it explicitly. Try @code{(std array)} first. @code{array-sum}, @code{array-dot}, @code{array-min}, @code{array-max}, @code{array-count}, @code{array-index-of}, @code{array-add!} and @code{array-scale!} are each one vector loop in the runtime, with the tail handled for you. On 4 million elements a sum of doubles took 0.8 ms instead of 2.6 ms for a loop, and a sum of ints 0.21 ms instead of 0.88 ms.