| // Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| // | |
| /// Benchmark for UTF-8 decoding, complex cases. | |
| /// | |
| /// This benckmark complements the `Utf8Decode` benchmarks by exploring | |
| /// different scenarios. There are three axes of variation - input complexity, | |
| /// conversion type, and polymorphism. The variantions are represented in the | |
| /// benchmark name, roughly | |
| /// | |
| /// Utf8DecodeComplex.<polymorphism>.<conversion>.<data>.<complexity>.10k | |
| /// | |
| /// ### Complexity | |
| /// | |
| /// The input complexity is explored by having input sequences that are the | |
| /// UTF-8 encoding of (1) ASCII strings ('ascii'), (2) strings that can be | |
| /// represented by one-byte strings ('1byte'), and (3) strings need to be | |
| /// represented by two-byte strings ('2byte'). | |
| /// | |
| /// Both of of these benchmarks process 10k bytes of input: | |
| /// | |
| /// Utf8DecodeComplex.mono.simple.ascii.10k | |
| /// Utf8DecodeComplex.mono.simple.2byte.10k | |
| /// | |
| /// The first has ascii bytes as input, the simplest case. The second requires | |
| /// parsing the variable-length UTF-8 code points. | |
| /// | |
| /// ### Conversion | |
| /// | |
| /// The conversion variations are 'simple', for a one-shot conversion of a List | |
| /// of bytes to a string, and 'chunked' for a conversion that uses the chunked | |
| /// conversion API to process the 10k bytes in chunks of a few hundred | |
| /// bytes. This exercises different paths through the decoder. We would expect | |
| /// the chuncked version to be slower, but only by a few percent. | |
| /// | |
| /// ### Data | |
| /// | |
| /// The type of the input is part of the benchmark name. When the input is a | |
| /// modifiable `Unit8List`, there is no `.<data>` part to the name. Otherwise: | |
| /// | |
| /// .list - Input is a system List (`List.of`) | |
| /// .unmodifiable - Input is an ummodifiable `Uint8List`. | |
| /// | |
| /// ### Polymorphism | |
| /// | |
| /// Polymorphism is explored by compiling several programs that run different | |
| /// subsets of the benchmarks. | |
| /// | |
| /// Whole-program optimizing compilers like AOT or dart2js can sometimes 'see' | |
| /// that the conversion code is called with a single implementation of List and | |
| /// optimize the code accordingly. This can produce faster code, but might be | |
| /// too optimistic as prediction of real-world performance. | |
| /// | |
| /// These two benchmarks run the same code, on a `Uint8List` containing the same | |
| /// values. Other than the name, the benchmarks are identical: | |
| /// | |
| /// Utf8DecodeComplex.mono.simple.ascii.10k | |
| /// Utf8DecodeComplex.poly.simple.ascii.10k | |
| /// | |
| /// The difference is that the 'mono' benchmark is part of a program that passes | |
| /// only modifiable `Uint8List` lists to `utf8.decode`, whereas the 'poly' | |
| /// benchmark is part of a program that passes several different List | |
| /// implementation type to `utf8.decode`, including system lists (`List.of`) and | |
| /// and unmodifiable Uint8Lists. | |
| /// | |
| /// There are three monomorphic entry points which are called from the `main` | |
| /// method of an otherwise trivial program - `mainMono1`, `mainMono2` and | |
| /// `mainMono3`. `mainMono1` does conversions exclusively on the preferred data | |
| /// type, `Uint8List`. `mainMono2` does conversions exclusively on the system | |
| /// list type (`List.of`). `mainMono3` does conversions exclusively on | |
| /// unmodifiable `Uint8List`. | |
| /// | |
| /// The primary program calls the `mainPoly` entry point. | |
| /// | |
| /// Benchmark results from the different programs can be collected into a single | |
| /// suite. | |
| library; | |
| import 'dart:convert'; | |
| import 'dart:math' show min; | |
| import 'dart:typed_data'; | |
| import 'package:benchmark_harness/benchmark_harness.dart'; | |
| import 'package:expect/expect.dart'; | |
| // ASCII values which are start the sequence for quick validation that | |
| // conversion happened. | |
| const bytes0tag = 0x30; | |
| const bytes1tag = 0x31; | |
| const bytes2tag = 0x32; | |
| // Input which decodes to a string where all code units are 7-bit ASCII. | |
| const bytes0 = [ | |
| bytes0tag, // 0, U+0030 | |
| 0x48, 0x45, 0x4C, 0x4C, 0x4f, 0x0A, // "HELLO\n" | |
| ]; | |
| // Input which decodes to a string where all code units fit in 1 byte. | |
| const bytes1 = [ | |
| bytes1tag, // 1, U+0031 | |
| 0x41, // A, U+0040 | |
| 0xC2, 0xB1, // ±, U+00B1 | |
| 0xC3, 0xB7, // ÷, U+00F7 | |
| 0x0A, // \n, U+000A | |
| ]; | |
| // Input which decodes to a string where some code units require 2 bytes. | |
| const bytes2 = [ | |
| bytes2tag, // 2, U+0032 | |
| 0x41, // A, U+0040 | |
| 0xC2, 0xB1, // ±, U+00B1 | |
| 0xC3, 0xB7, // ÷, U+00F7 | |
| 0xC4, 0x90, // Đ, U+0111 | |
| 0xE0, 0xA6, 0x86, // আ, U+0986 | |
| 0xF0, 0x9F, 0x87, 0xB9, // 🇹, U+1F1F9 | |
| 0xF0, 0x9F, 0x87, 0xBB, // 🇻, U+1F1FB 🇹🇻 | |
| ]; | |
| const targetSize = 10000; | |
| const chunkSize = 250; | |
| void check(String result, List<int> sequence) { | |
| // Each sequence starts with a different ASCII value so we can quickly 'look | |
| // up' the expected length of the decoded expanded sequence. | |
| final expectedLength = switch (sequence[0]) { | |
| bytes0tag => targetSize, | |
| bytes1tag => 7144, | |
| bytes2tag => 5266, | |
| _ => throw 'Unexpected sequence start: ${sequence[0]}', | |
| }; | |
| Expect.equals(expectedLength, result.length); | |
| } | |
| /// Expands a sequence by repetition and padding to `targetSize` bytes. | |
| Uint8List makeSequence(List<int> bytes) { | |
| Expect.equals( | |
| 1, | |
| bytes.length.gcd(chunkSize), | |
| 'Bad repeated size (${bytes.length}).' | |
| ' Repeated sequence should be co-prime with chunk size ($chunkSize)' | |
| ' to exercise more UTF-8 boundaries', | |
| ); | |
| final repeats = List.filled( | |
| targetSize ~/ bytes.length, | |
| bytes, | |
| ).expand((byte) => byte); | |
| final padding = List.filled(targetSize.remainder(bytes.length), 0); // NULs. | |
| final sequence = Uint8List.fromList([...repeats, ...padding]); | |
| Expect.equals(targetSize, sequence.length); | |
| return sequence; | |
| } | |
| final Uint8List sequence0 = makeSequence(bytes0); | |
| final Uint8List sequence1 = makeSequence(bytes1); | |
| final Uint8List sequence2 = makeSequence(bytes2); | |
| class Utf8DecodeBenchmarkBase extends BenchmarkBase { | |
| Utf8DecodeBenchmarkBase(String name) : super('Utf8DecodeComplex.$name'); | |
| late int totalInputSize; | |
| @override | |
| void exercise() { | |
| // Only a single run per measurement instead of the usual 10. | |
| run(); | |
| } | |
| @override | |
| double measure() { | |
| // Report time per input byte. | |
| return super.measure() / totalInputSize; | |
| } | |
| @override | |
| void report() { | |
| // Report time in nanoseconds. | |
| final double score = measure() * 1000.0; | |
| print('$name(RunTime): $score ns.'); | |
| } | |
| } | |
| class Simple extends Utf8DecodeBenchmarkBase { | |
| final List<int> sequence; | |
| Simple(super.name, this.sequence) { | |
| totalInputSize = sequence.length; | |
| } | |
| @override | |
| void run() { | |
| final result = utf8.decode(sequence); | |
| check(result, sequence); | |
| } | |
| } | |
| abstract class ChunkedBase extends Utf8DecodeBenchmarkBase { | |
| final List<int> sequence; | |
| late List<List<int>> chunks; | |
| ChunkedBase(super.name, this.sequence); | |
| List<int> slice(List<int> list, int start, int end); | |
| @override | |
| void setup() { | |
| totalInputSize = sequence.length; | |
| chunks = []; | |
| for (int i = 0; i < totalInputSize; i += chunkSize) { | |
| final chunk = slice(sequence, i, min(i + chunkSize, totalInputSize)); | |
| chunks.add(chunk); | |
| } | |
| } | |
| @override | |
| void run() { | |
| late final String result; | |
| final byteSink = const Utf8Decoder().startChunkedConversion( | |
| StringConversionSink.withCallback((s) => result = s), | |
| ); | |
| for (final chunk in chunks) { | |
| byteSink.add(chunk); | |
| } | |
| byteSink.close(); | |
| check(result, sequence); | |
| } | |
| } | |
| class Chunked extends ChunkedBase { | |
| Chunked(super.name, super.sequence); | |
| @override | |
| List<int> slice(List<int> list, int start, int end) { | |
| return list.sublist(start, end); | |
| } | |
| } | |
| class ChunkedUnmodifiable extends ChunkedBase { | |
| ChunkedUnmodifiable(super.name, Uint8List super.sequence); | |
| @override | |
| Uint8List slice(List<int> list, int start, int end) { | |
| return Uint8List.fromList(list.sublist(start, end)).asUnmodifiableView(); | |
| } | |
| } | |
| void runAll(List<BenchmarkBase> benchmarks) { | |
| // Warm up all the benchmarks to avoid overly optimistic results for the first | |
| // few benchmarks due to temporary monomorphism in JIT compilers. | |
| for (var bm in benchmarks) { | |
| bm.setup(); | |
| bm.warmup(); | |
| } | |
| for (var bm in benchmarks) { | |
| bm.report(); | |
| } | |
| } | |
| void mainPoly() { | |
| // Polymorphic: Inputs of several types. | |
| final benchmarks = [ | |
| Simple('poly.simple.ascii.10k', sequence0), | |
| Simple('poly.simple.1byte.10k', sequence1), | |
| Simple('poly.simple.2byte.10k', sequence2), | |
| Simple('poly.simple.list.ascii.10k', List.of(sequence0)), | |
| Simple('poly.simple.list.1byte.10k', List.of(sequence1)), | |
| Simple('poly.simple.list.2byte.10k', List.of(sequence2)), | |
| Simple( | |
| 'poly.simple.unmodifiable.ascii.10k', | |
| sequence0.asUnmodifiableView(), | |
| ), | |
| Simple( | |
| 'poly.simple.unmodifiable.1byte.10k', | |
| sequence1.asUnmodifiableView(), | |
| ), | |
| Simple( | |
| 'poly.simple.unmodifiable.2byte.10k', | |
| sequence2.asUnmodifiableView(), | |
| ), | |
| Chunked('poly.chunked.ascii.10k', sequence0), | |
| Chunked('poly.chunked.1byte.10k', sequence1), | |
| Chunked('poly.chunked.2byte.10k', sequence2), | |
| Chunked('poly.chunked.list.ascii.10k', List.of(sequence0)), | |
| Chunked('poly.chunked.list.1byte.10k', List.of(sequence1)), | |
| Chunked('poly.chunked.list.2byte.10k', List.of(sequence2)), | |
| ChunkedUnmodifiable( | |
| 'poly.chunked.unmodifiable.ascii.10k', | |
| sequence0.asUnmodifiableView(), | |
| ), | |
| ChunkedUnmodifiable( | |
| 'poly.chunked.unmodifiable.1byte.10k', | |
| sequence1.asUnmodifiableView(), | |
| ), | |
| ChunkedUnmodifiable( | |
| 'poly.chunked.unmodifiable.2byte.10k', | |
| sequence2.asUnmodifiableView(), | |
| ), | |
| ]; | |
| runAll(benchmarks); | |
| } | |
| void mainMono1() { | |
| // Monomorphic: All inputs are `Uint8List`s. | |
| final benchmarks = [ | |
| Simple('mono.simple.ascii.10k', sequence0), | |
| Simple('mono.simple.1byte.10k', sequence1), | |
| Simple('mono.simple.2byte.10k', sequence2), | |
| Chunked('mono.chunked.ascii.10k', sequence0), | |
| Chunked('mono.chunked.1byte.10k', sequence1), | |
| Chunked('mono.chunked.2byte.10k', sequence2), | |
| ]; | |
| runAll(benchmarks); | |
| } | |
| void mainMono2() { | |
| // Monomorphic: All inputs are ordinary (system) Lists. | |
| final benchmarks = [ | |
| Simple('mono.simple.list.ascii.10k', List.of(sequence0)), | |
| Simple('mono.simple.list.1byte.10k', List.of(sequence1)), | |
| Simple('mono.simple.list.2byte.10k', List.of(sequence2)), | |
| Chunked('mono.chunked.list.ascii.10k', List.of(sequence0)), | |
| Chunked('mono.chunked.list.1byte.10k', List.of(sequence1)), | |
| Chunked('mono.chunked.list.2byte.10k', List.of(sequence2)), | |
| ]; | |
| runAll(benchmarks); | |
| } | |
| void mainMono3() { | |
| // Monomorphic: All inputs are unmodifiable `Uint8List`s. | |
| final benchmarks = [ | |
| Simple( | |
| 'mono.simple.unmodifiable.ascii.10k', | |
| sequence0.asUnmodifiableView(), | |
| ), | |
| Simple( | |
| 'mono.simple.unmodifiable.1byte.10k', | |
| sequence1.asUnmodifiableView(), | |
| ), | |
| Simple( | |
| 'mono.simple.unmodifiable.2byte.10k', | |
| sequence2.asUnmodifiableView(), | |
| ), | |
| ChunkedUnmodifiable( | |
| 'mono.chunked.unmodifiable.ascii.10k', | |
| sequence0.asUnmodifiableView(), | |
| ), | |
| ChunkedUnmodifiable( | |
| 'mono.chunked.unmodifiable.1byte.10k', | |
| sequence1.asUnmodifiableView(), | |
| ), | |
| ChunkedUnmodifiable( | |
| 'mono.chunked.unmodifiable.2byte.10k', | |
| sequence2.asUnmodifiableView(), | |
| ), | |
| ]; | |
| runAll(benchmarks); | |
| } |