diff --git a/_02_.Writing-programs.sz b/_02_.Writing-programs.sz index 5885198..d6d4bd9 100644 --- a/_02_.Writing-programs.sz +++ b/_02_.Writing-programs.sz @@ -375,6 +375,39 @@ You can pull in third-party .NET packages directly from NuGet by listing them in Once restored, types and methods from NuGet packages can be consumed through Bjolang's .NET interop mechanisms (described in the .NET section below). Version strings follow standard NuGet versioning semantics: @code{"9.0.3"} matches version 9.0.3 or higher, while bracketed syntax like @code{"[9.0.3]"} locks to that exact version. Bjolang delegates package restoration to the .NET SDK via the @code{dotnet} tool, storing the resolution graph in @code{packages.lock.json} (which should also be committed). Currently, compiled binaries load dependencies directly from the local NuGet cache, so executables expect to run in an environment with the restored packages available. +@subsubsection{Native executables} + +A program usually runs on .NET: @code{bjo run} starts the runtime, and the JIT compiles each method the first time it runs. With @code{(aot #t)} in the manifest, the program is also compiled ahead of time, with .NET's Native AOT, to one native executable: + +@codeblock[#:lang "bjolang"]|{ +(package + (name (cards)) + (version "0.1.0") + (aot #t)) +}| + +The executable is beside the source, with no extension: @code{src/main}, or @code{src/main.native.exe} on Windows. It holds the program, the standard library and the .NET runtime, so it runs on a machine without .NET installed. + +@read-list{ + - @strong{It starts in a few milliseconds.} A program on .NET spends about 70 ms starting the runtime, and then compiles its methods as it runs them. On 17 days of Advent of Code, the solutions together took 3.0 s on .NET and 1.2 s as native executables. + - @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. +} + +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: + +@codeblock[#:lang "bjolang"]|{ + (aot #t) + (aot-cpu "native") +}| + +@code{"native"} is the CPU of the machine that builds. A level, such as @code{"x86-64-v3"}, is every CPU that has its instructions: @code{x86-64-v3} has AVX2, and @code{x86-64-v4} has AVX-512. An executable built for a CPU stops at start on a CPU that does not have the instructions, so use @code{"native"} for a program that runs where it is built. This matters most to @code{(std array)} and @code{(std simd)}; see the section on vectors under performance. + +Only the project being built reads @code{aot} and @code{aot-cpu}, as with @code{runtime}: a dependency is not a program. A change to either rebuilds the program. The compiler itself takes them as @code{--aot} and @code{--aot-cpu}, for a file built without a project. + @subsubsection{Publishing a package} Publishing a Bjolang package requires three basic steps: @@ -2104,3 +2137,41 @@ Use a @code{Vec} for everything else. It knows its length and last item, it read Use a @code{Map} to look up, update, insert and remove by key. It wins at all of those at every size. The difference is largest for string keys, because a map hashes the key once and an ordered map compares it at every step down the tree. 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} +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. + +For a loop that the kernels do not cover, write it with @code{(std simd)}. A loop over an array has two parts: whole vectors, and the elements after the last whole vector, one at a time: + +@codeblock[#:lang "bjolang"]|{ +(import (std simd)) + +(: dot (-> (Array double) (Array double) double)) +(defun (dot a b) + (def w (simd256-lanes a)) ; 4 doubles in a Float64x4 + (def whole (simd256-whole a)) ; where the tail starts + (def acc + (loop (:for i (up-from 0 #:to whole #:by w)) + (:acc v (folding (simd256-zero) + (simd-add v (simd-mul (simd256-load a i) (simd256-load b i))))) + => v)) + (+ (simd-sum acc) + (loop (:for i (up-from whole #:to (array-length a))) + (:acc s (folding 0.0 (+ s (* (array-ref a i) (array-ref b i))))) + => s))) +}| + +A comparison gives a mask instead of a bool: @code{(simd-lt v limit)} has every bit of a lane set where the lane is less. Use @code{simd-select} to choose lanes by a mask, @code{simd-count-true} to count them, and @code{simd-and} and @code{simd-or} to combine masks. A branch in a scalar loop is often a select in a vector loop. + +Some things to keep in mind: + +@read-list{ + - Use more than one accumulator in a sum. An addition waits for the one before it. On 4 million doubles the loop above took 2.9 ms, the same with four @code{Float64x4} accumulators 1.4 ms, and a scalar loop 9.9 ms. + - Put a margin of zeros round a grid. A load at a neighbour's offset then needs no bounds test, and the zeros match nothing. Make each row longer than the grid by a whole vector, so that the last vector of a row does not read the next row. + - A vector loop pays when the work is dense and does not stop early. On Advent of Code 2025 day 4, a neighbour count over a 3000x3000 grid, the vectors were 9 times faster than the same loop one byte at a time. On day 9, a search that usually stops after a few comparisons, they were 1.6 times faster. On a small input, reading the input takes almost all of the time, and vectors change nothing. The @code{-avx} solutions in @code{Examples/aoc} show all three. + - Use 128 bits for code that must run everywhere. A 256-bit vector is emulated on a CPU without AVX2, and on ARM64, and is then much slower than scalar code. @code{simd256-accelerated?} says which you have. + - A Native AOT build targets a baseline x86-64 CPU, without AVX2, unless the manifest says @code{(aot-cpu "native")} or a level such as @code{(aot-cpu "x86-64-v3")}. The executable then does not start on a CPU without those instructions. A program that runs on .NET uses what the CPU has. + - Shifts, conversions between element types and shuffles are not there yet. +} diff --git a/_03.Standard-library.sz b/_03.Standard-library.sz index eed722e..907937b 100644 --- a/_03.Standard-library.sz +++ b/_03.Standard-library.sz @@ -12,6 +12,11 @@ For more details, see @link["modules/std/prelude.html"]{@code{(std prelude)}}. Numbers beyond the operators. Integer division has Racket's names: @code{quotient} and @code{remainder} are what @code{/} and @code{%} do, and @code{modulo} takes the sign of the divisor, so @code{(modulo -1 10)} is 9. There are @code{gcd}, @code{lcm}, an integer @code{expt} and an exact @code{integer-sqrt} at every integer type, the @code{exact-} roundings from a double to an int, and the rest of @code{System.Math} over doubles. The prelude has @code{abs}, @code{min}, @code{max} and @code{clamp} from here without an import. For more details, see @link["modules/std/maths.html"]{@code{(std maths)}}. +@subsection{std/array and std/simd} + +Two modules for numbers in bulk, with the CPU's vector instructions. @code{(std array)} has whole-array operations: @code{array-sum}, @code{array-dot}, @code{array-min}, @code{array-max}, @code{array-add!}, @code{array-scale!}, @code{array-count}, @code{array-index-of} and @code{array-fill!}. Each is one loop in the runtime, which works on as many elements per instruction as a vector register holds when the element is a primitive number, and one at a time for a type such as @code{bigint}. At @code{double}, a sum adds lane by lane, so its last bits can differ from a left fold. @code{(std simd)} has the vectors themselves, for the loops that the kernels do not cover. @code{(Simd128 %a)} and @code{(Simd256 %a)} are .NET's @code{Vector128} and @code{Vector256}, with a name per element type: @code{Float64x4} is four doubles, @code{UInt8x32} is 32 bytes. The operations are the methods of one trait, @code{Simd}, at both widths: @code{simd-add}, @code{simd-mul}, the comparisons @code{simd-lt} and the others, which give masks, @code{simd-select}, @code{simd-count-true}, @code{simd-sum} and more. A vector is made with @code{simd256-load}, @code{simd128-splat} or @code{simd256-zero}, and @code{simd256-whole} says where the elements after the last whole vector start. A 128-bit vector is fast on every x86-64 and ARM64 CPU. A 256-bit vector needs AVX2, and @code{simd256-accelerated?} says whether the CPU has it. The section on performance in "Writing programs" says when vectors pay. +For more details, see @link["modules/std/array.html"]{@code{(std array)}} and @link["modules/std/simd.html"]{@code{(std simd)}}. + @subsection{std/fmt} Formatting in Bjolang is divided into two layers: interpolation and layout. diff --git a/modules/std/array.sz b/modules/std/array.sz new file mode 100644 index 0000000..5ea4468 --- /dev/null +++ b/modules/std/array.sz @@ -0,0 +1,3 @@ +(meta (title "(std array)")) + +@generate-docs[#:module "std array"] diff --git a/modules/std/simd.sz b/modules/std/simd.sz new file mode 100644 index 0000000..4f9ca61 --- /dev/null +++ b/modules/std/simd.sz @@ -0,0 +1,3 @@ +(meta (title "(std simd)")) + +@generate-docs[#:module "std simd"]