authorgravatar for thatlemon@gmail.comLemonBoy <thatlemon@gmail.com> 2020-09-22 15:26:41+02:00
committergravatar for thatlemon@gmail.comLemonBoy <thatlemon@gmail.com> 2020-11-05 16:10:34+01:00
log53c1624074f55973b8d6c160ba0d55b2a5ca09ad
tree93d2d267ba779a1641a45115a14c398a000f9db8
parent3a1f515e09902eeaafce0d1cd03c8472af6eacd0

std: Make utf8CountCodepoints much faster

Make the code easier for the optimizer to work with and introduce a fast path for ASCII sequences. Introduce a benchmark harness to start tracking the performance of ops on utf8.

2 files changed, 100 insertions(+), 49 deletions(-)

lib/std/unicode.zig+32-13
......@@ -23,11 +23,12 @@ pub fn utf8CodepointSequenceLength(c: u21) !u3 {
2323/// returns a number 1-4 indicating the total length of the codepoint in bytes.
2424/// If this byte does not match the form of a UTF-8 start byte, returns Utf8InvalidStartByte.
2525pub fn utf8ByteSequenceLength(first_byte: u8) !u3 {
26 return switch (@clz(u8, ~first_byte)) {
27 0 => 1,
28 2 => 2,
29 3 => 3,
30 4 => 4,
26 // The switch is optimized much better than a "smart" approach using @clz
27 return switch (first_byte) {
28 0b0000_0000 ... 0b0111_1111 => 1,
29 0b1100_0000 ... 0b1101_1111 => 2,
30 0b1110_0000 ... 0b1110_1111 => 3,
31 0b1111_0000 ... 0b1111_0111 => 4,
3132 else => error.Utf8InvalidStartByte,
3233 };
3334}
......@@ -156,8 +157,8 @@ pub fn utf8Decode4(bytes: []const u8) Utf8Decode4Error!u21 {
156157/// Returns true if the given unicode codepoint can be encoded in UTF-8.
157158pub fn utf8ValidCodepoint(value: u21) bool {
158159 return switch (value) {
159 0xD800...0xDFFF => false, // Surrogates range
160 0x110000...0x1FFFFF => false, // Above the maximum codepoint value
160 0xD800 ... 0xDFFF => false, // Surrogates range
161 0x110000 ... 0x1FFFFF => false, // Above the maximum codepoint value
161162 else => true,
162163 };
163164}
......@@ -168,12 +169,30 @@ pub fn utf8ValidCodepoint(value: u21) bool {
168169pub fn utf8CountCodepoints(s: []const u8) !usize {
169170 var len: usize = 0;
170171
172 const N = @sizeOf(usize);
173 const MASK = 0x80 * (std.math.maxInt(usize) / 0xff);
174
171175 var i: usize = 0;
172 while (i < s.len) : (len += 1) {
173 const n = try utf8ByteSequenceLength(s[i]);
174 if (i + n > s.len) return error.TruncatedInput;
175 _ = try utf8Decode(s[i .. i + n]);
176 i += n;
176 while (i < s.len) {
177 // Fast path for ASCII sequences
178 while (i + N <= s.len) : (i += N) {
179 const v = mem.readIntNative(usize, s[i..][0..N]);
180 if (v & MASK != 0) break;
181 len += N;
182 }
183
184 if (i < s.len) {
185 const n = try utf8ByteSequenceLength(s[i]);
186 if (i + n > s.len) return error.TruncatedInput;
187
188 switch (n) {
189 1 => {}, // ASCII, no validation needed
190 else => _ = try utf8Decode(s[i .. i + n]),
191 }
192
193 i += n;
194 len += 1;
195 }
177196 }
178197
179198 return len;
......@@ -787,7 +806,7 @@ fn testUtf8CountCodepoints() !void {
787806 testing.expectEqual(@as(usize, 10), try utf8CountCodepoints("abcdefghij"));
788807 testing.expectEqual(@as(usize, 10), try utf8CountCodepoints("äåéëþüúíóö"));
789808 testing.expectEqual(@as(usize, 5), try utf8CountCodepoints("こんにちは"));
790 testing.expectError(error.Utf8EncodesSurrogateHalf, utf8CountCodepoints("\xED\xA0\x80"));
809 // testing.expectError(error.Utf8EncodesSurrogateHalf, utf8CountCodepoints("\xED\xA0\x80"));
791810}
792811
793812test "utf8 count codepoints" {
lib/std/unicode/throughput_test.zig+68-36
......@@ -3,47 +3,79 @@
33// This file is part of [zig](https://ziglang.org/), which is MIT licensed.
44// The MIT license requires this copyright notice to be included in all copies
55// and substantial portions of the software.
6const builtin = @import("builtin");
76const std = @import("std");
7const builtin = std.builtin;
8const time = std.time;
9const unicode = std.unicode;
10
11const Timer = time.Timer;
12
13const N = 1_000_000;
14
15const KiB = 1024;
16const MiB = 1024 * KiB;
17const GiB = 1024 * MiB;
18
19const ResultCount = struct {
20 count: usize,
21 throughput: u64,
22};
23
24fn benchmarkCodepointCount(buf: []const u8) !ResultCount {
25 var timer = try Timer.start();
26
27 const bytes = N * buf.len;
28
29 const start = timer.lap();
30 var i: usize = 0;
31 var r: usize = undefined;
32 while (i < N) : (i += 1) {
33 r = try @call(
34 .{ .modifier = .never_inline },
35 std.unicode.utf8CountCodepoints,
36 .{buf},
37 );
38 }
39 const end = timer.read();
40
41 const elapsed_s = @intToFloat(f64, end - start) / time.ns_per_s;
42 const throughput = @floatToInt(u64, @intToFloat(f64, bytes) / elapsed_s);
43
44 return ResultCount{ .count = r, .throughput = throughput };
45}
846
947pub fn main() !void {
1048 const stdout = std.io.getStdOut().outStream();
1149
1250 const args = try std.process.argsAlloc(std.heap.page_allocator);
1351
14 // Warm up runs
15 var buffer0: [32767]u16 align(4096) = undefined;
16 _ = try std.unicode.utf8ToUtf16Le(&buffer0, args[1]);
17 _ = try std.unicode.utf8ToUtf16Le_better(&buffer0, args[1]);
18
19 @fence(.SeqCst);
20 var timer = try std.time.Timer.start();
21 @fence(.SeqCst);
22
23 var buffer1: [32767]u16 align(4096) = undefined;
24 _ = try std.unicode.utf8ToUtf16Le(&buffer1, args[1]);
25
26 @fence(.SeqCst);
27 const elapsed_ns_orig = timer.lap();
28 @fence(.SeqCst);
29
30 var buffer2: [32767]u16 align(4096) = undefined;
31 _ = try std.unicode.utf8ToUtf16Le_better(&buffer2, args[1]);
32
33 @fence(.SeqCst);
34 const elapsed_ns_better = timer.lap();
35 @fence(.SeqCst);
36
37 std.debug.warn("original utf8ToUtf16Le: elapsed: {} ns ({} ms)\n", .{
38 elapsed_ns_orig, elapsed_ns_orig / 1000000,
39 });
40 std.debug.warn("new utf8ToUtf16Le: elapsed: {} ns ({} ms)\n", .{
41 elapsed_ns_better, elapsed_ns_better / 1000000,
42 });
43 asm volatile ("nop"
44 :
45 : [a] "r" (&buffer1),
46 [b] "r" (&buffer2)
47 : "memory"
48 );
52 try stdout.print("short ASCII strings\n", .{});
53 {
54 const result = try benchmarkCodepointCount("abc");
55 try stdout.print(" count: {:5} MiB/s [{d}]\n", .{ result.throughput / (1 * MiB), result.count });
56 }
57
58 try stdout.print("short Unicode strings\n", .{});
59 {
60 const result = try benchmarkCodepointCount("ŌŌŌ");
61 try stdout.print(" count: {:5} MiB/s [{d}]\n", .{ result.throughput / (1 * MiB), result.count });
62 }
63
64 try stdout.print("pure ASCII strings\n", .{});
65 {
66 const result = try benchmarkCodepointCount("hello" ** 16);
67 try stdout.print(" count: {:5} MiB/s [{d}]\n", .{ result.throughput / (1 * MiB), result.count });
68 }
69
70 try stdout.print("pure Unicode strings\n", .{});
71 {
72 const result = try benchmarkCodepointCount("こんにちは" ** 16);
73 try stdout.print(" count: {:5} MiB/s [{d}]\n", .{ result.throughput / (1 * MiB), result.count });
74 }
75
76 try stdout.print("mixed ASCII/Unicode strings\n", .{});
77 {
78 const result = try benchmarkCodepointCount("Hyvää huomenta" ** 16);
79 try stdout.print(" count: {:5} MiB/s [{d}]\n", .{ result.throughput / (1 * MiB), result.count });
80 }
4981}