# mbw 2.0 (raas/mbw, meta-oe recipes-benchmark, SRCREV c3155b54) -- the copy # memory-bandwidth benchmark. Three copy methods over two heap arrays: # -t0 MEMCPY one memcpy() of the whole array # -t1 DUMB b[i]=a[i] over long-sized elements # -t2 MCBLOCK memcpy() in fixed blocks (default 262144 B) # It is the third benchmark on this image, after dhrystone and libc-bench, and # the first that measures the RUNTIME rather than the libc: every byte it moves # goes through the wasm linear-memory load/store path, so the numbers are a # property of hardwarejs, not of the program. # # Sizes are deliberately small (2-8 MiB). Guest memory is not returned to the # kernel on process exit (brintos/linux#25), so a suite that allocated the # hundreds of MiB mbw is normally pointed at would starve every suite scheduled # behind it in the same boot. . "$BST_LIB" require mbw mbw/present W=$(mktemp -d); [ -d "$W" ] || W=/tmp/bst-mbw.$$ mkdir -p "$W" # AVG "Copy:" column (MiB/s) for one method out of an mbw run. rate() { awk -v m="$2" '$1=="AVG" && $3==m {print $9}' "$1"; } # --- the tool answers for itself --------------------------------------------- # Each of these exits 1 BY DESIGN -- usage and the two argument refusals are # error paths -- so the message is asserted over a captured file (cat exits 0) # and the status with check_rc. check_out would conflate the two and report a # correct diagnostic as a failure. mbw -h > "$W/h" 2>&1 check_out mbw/usage "mbw memory benchmark v1.5" cat "$W/h" check_rc mbw/usage-rc 1 mbw -h # Both argument refusals, because they are different code paths: one is "no # operand at all", the other is an operand that parses to zero. A shell that # mangled argv would break exactly one of them. mbw > "$W/noarg" 2>&1 check_out mbw/no-arg-refused "Error: no array size given!" cat "$W/noarg" check_rc mbw/no-arg-rc 1 mbw mbw 0 > "$W/zero" 2>&1 check_out mbw/zero-size-refused "Error: array size wrong!" cat "$W/zero" check_rc mbw/zero-size-rc 1 mbw 0 # -t2 needs the array to be at least one block long; the guard fires when it is # not. (Upstream's message has the comparison backwards -- the array is SMALLER # than the block -- so match the invariant part only.) mbw -t2 -b 8388608 1 > "$W/blk" 2>&1 check_out mbw/block-guard "Error: array size larger than block size" cat "$W/blk" check_rc mbw/block-guard-rc 1 mbw -t2 -b 8388608 1 # --- this arm is ILP32, and mbw is the tool that says so out loud ------------- # sizeof(long) is 4 here against 8 on the x86_64 build host, so the same MiB # figure means twice the element count. mbw prints both, which makes it a # one-line check that the data model is what the rest of the image assumes. mbw -n1 -t0 2 > "$W/hdr" 2>&1 check_out mbw/long-is-4-bytes "Long uses 4 bytes." cat "$W/hdr" check_out mbw/alloc-reported "Allocating 2*524288 elements = 4194304 bytes of memory." cat "$W/hdr" # --- each method runs, and is selectable ------------------------------------- # Asserted per method rather than once over a default run, so a failure names # WHICH copy path broke. All three go through different code: one libc call, a # compiler-generated element loop, and a libc call in a driver loop. check_out mbw/method-memcpy "Method: MEMCPY" mbw -n1 -t0 2 check_out mbw/method-dumb "Method: DUMB" mbw -n1 -t1 2 check_out mbw/method-mcblock "Method: MCBLOCK" mbw -n1 -t2 2 check_rc mbw/exit-clean 0 mbw -n1 -t0 2 # The default run is all three methods with no -t at all -- a separate path from # the three selected runs above, and the one every casual invocation takes. mbw -n3 4 > "$W/all" 2>&1 miss="" for m in MEMCPY DUMB MCBLOCK; do grep -q "Method: $m" "$W/all" || miss="$miss $m" done if [ -z "$miss" ]; then bst_ok mbw/all-three-methods else bst_notok mbw/all-three-methods "default run missing:$miss"; fi # -n is honoured: 3 numbered lines plus one AVG, per method. n_mem=$(awk '$1 ~ /^[0-9]+$/ && $3=="MEMCPY"' "$W/all" | wc -l) check_eq mbw/loop-count-honoured 3 "$(echo $n_mem)" n_avg=$(grep -c '^AVG' "$W/all") check_eq mbw/avg-per-method 3 "$(echo $n_avg)" # -q drops the two header lines but keeps the measurements. A tool whose quiet # mode also dropped results would still pass every case above. mbw -q -n1 -t0 2 > "$W/q" 2>&1 if grep -q "Long uses" "$W/q"; then bst_notok mbw/quiet-suppresses-header "-q still printed the header" else check_out mbw/quiet-keeps-results "Method: MEMCPY" cat "$W/q" bst_ok mbw/quiet-suppresses-header fi check_out mbw/block-size-honoured "Using 65536 bytes as blocks" mbw -n1 -t2 -b 65536 2 # --- the measurements are real ------------------------------------------------ # The point of contrast with dhrystone, which cannot time itself on this arm at # all (times(2) reads 0 for a live task -- brintos/linux#38). mbw times with # gettimeofday(), which is host-backed and advances mid-run, so every Elapsed # must be strictly positive. A zero here would not merely lose a number: it # divides into the MiB/s column and prints "inf", so the suite would still look # like it was reporting bandwidth. zeros=$(awk '/Elapsed:/ { for (i=1;i<=NF;i++) if ($i=="Elapsed:") { if ($(i+1)+0 <= 0) print $(i+1) } }' "$W/all") if [ -z "$zeros" ]; then bst_ok mbw/timings-positive else bst_notok mbw/timings-positive "$(_bst_1line "non-positive Elapsed: $zeros")"; fi # And the derived column is a finite number rather than inf/nan, which is what a # zero Elapsed would actually produce. bad=$(awk '{ for (i=1;i<=NF;i++) if ($i=="Copy:") { v=$(i+1); if (v ~ /inf|nan/ || v+0 <= 0) print v } }' "$W/all") if [ -z "$bad" ]; then bst_ok mbw/bandwidth-finite else bst_notok mbw/bandwidth-finite "$(_bst_1line "non-finite Copy: $bad")"; fi # Sanity floor, not a performance gate: 50 MiB/s is two orders of magnitude # under what this arm measures and an order under the slowest plausible copy. # It exists to catch a timer that has started reporting seconds-per-byte, not # to fail the day the runtime gets slower. mem=$(rate "$W/all" MEMCPY) if awk -v r="$mem" 'BEGIN{exit !(r+0 > 50)}'; then bst_ok mbw/bandwidth-floor else bst_notok mbw/bandwidth-floor "MEMCPY AVG ${mem:-} MiB/s is under the 50 MiB/s floor" fi # --- EXPECTED RED: glibc's memcpy is slower than a plain element loop --------- # On an ordinary target memcpy() is at worst competitive with b[i]=a[i] -- it is # the routine libcs hand-write in asm precisely so a naive loop cannot beat it. # Here it loses, and not narrowly: DUMB measures 2.8-5.3x MEMCPY across 1-64 MiB # (3.3-4.1x at the 4 MiB this suite uses), where natively on the x86_64 build # host the two stay within ~25% of each other in either direction -- 4 MiB: # 18762 vs 14948 MiB/s, 8 MiB: 8466 vs 9810. # # The cause is visible in the shipped module. glibc's memcpy is its generic C # word-copy -- a byte prologue to align, then 4x/8x unrolled i32 load/store # loops, 704 instructions, and NOT ONE memory.copy. wasm's bulk-memory # memory.copy is available and enabled on this arm: the same mbw module already # contains 110 of them, emitted by the compiler for llvm.memcpy inside glibc's # own C sources. So the instruction that would make this one host-side move is # present and used elsewhere, and the out-of-line memcpy symbol every other # program on the image calls simply does not route to it. # # brintos/linux-in-the-browser#146. # # Asserted positively -- "memcpy is at least as fast as the dumb loop" -- so the # day glibc's string routines route to memory.copy this case goes green on its # own and nobody has to remember to delete an exclusion. dumb=$(rate "$W/all" DUMB) if [ -n "$mem" ] && [ -n "$dumb" ]; then if awk -v m="$mem" -v d="$dumb" 'BEGIN{exit !(m+0 >= d+0)}'; then bst_ok mbw/memcpy-not-slower-than-dumb else bst_notok mbw/memcpy-not-slower-than-dumb \ "$(awk -v m="$mem" -v d="$dumb" 'BEGIN{printf "MEMCPY %s < DUMB %s MiB/s (%.1fx) — glibc memcpy does not use memory.copy; linux-in-the-browser#146", m, d, d/m}')" fi else bst_notok mbw/memcpy-not-slower-than-dumb "no AVG rate parsed (MEMCPY='$mem' DUMB='$dumb')" fi rm -rf "$W"