Skip to content

Commit c242d3a

Browse files
jensjohaCommit Queue
authored and
Commit Queue
committed
[scanner] Use 'vm:unsafe:no-bounds-checks' and add explicit bounds checks
The (utf8) scanner currently has this thing where you give it a 0-terminated byte-array (i.e. you read the file, then allocate something that's 1 bigger, copy the data, then give it to the scanner) to 'avoid bounds checks'. Dart still has bounds checks though - they're just implicit. As for the string scanner ut gets a string, then creates a new string like `string + '\x00'` - so basically the same thing. This CL uses the 'vm:unsafe:no-bounds-checks' pragma, removing the implicit bounds checks, adding explicit bounds checks, saving ~73.6 mio instructions when compiling the CFE in the process: ``` Comparing snapshot #1 with snapshot #2 cycles:u: -0.9983% +/- 0.6563% (-174026333.30 +/- 114410028.98) instructions:u: -0.3416% +/- 0.0005% (-73659267.00 +/- 108567.20) branch-misses:u: -4.8952% +/- 2.2612% (-3172939.50 +/- 1465641.18) ``` With the scanner-benchmark with `--bytes` I get this: ``` msec task-clock:u: -1.2251% +/- 0.6355% (-50.64 +/- 26.27) cycles:u: -1.2376% +/- 0.6385% (-223642830.80 +/- 115393789.68) instructions:u: -2.8155% +/- 0.0000% (-1153243856.00 +/- 428.11) seconds time elapsed: -1.2165% +/- 0.6408% (-0.05 +/- 0.03) seconds user: -1.1539% +/- 0.6495% (-0.05 +/- 0.03) ``` With the scanner-benchmark with `--string` I get this: ``` msec task-clock:u: -7.6439% +/- 0.6628% (-366.08 +/- 31.74) page-faults:u: -95.0034% +/- 0.0014% (-228023.50 +/- 3.41) instructions:u: 2.1041% +/- 0.0000% (897941907.60 +/- 2082.79) branch-misses:u: 3.2994% +/- 1.4675% (3239735.30 +/- 1440940.88) seconds time elapsed: -7.6595% +/- 0.6610% (-0.37 +/- 0.03) seconds user: -0.8801% +/- 0.7676% (-0.04 +/- 0.03) seconds sys: -92.0140% +/- 2.8075% (-0.33 +/- 0.01) MarkSweep( old space) goes from 6 to 0 Notice combined GC time goes from 112 ms to 41 ms (notice only 1 run each). ``` Where I'll note that the 'vm:unsafe:no-bounds-checks' pragma doesn't (yet?) work for `String.codeUnitAt`. See https://dart-review.googlesource.com/c/sdk/+/384540 (and https://dart-review.googlesource.com/c/sdk/+/385201) for details. I assume the relatively big change here is caused by not allocating a new string with a 0-byte in the end each time. Note that the read-allocate-copy dance is still performed for the utf8 scanner in this CL as it requires changing all call-sites instead. It will be done in a follow-up CL where the "end-of-file" int will likely also be changed to `-1` to (I assume) allow for having the 0-byte in the middle of a file (see also the 10+ year old bug at #18090) Note: The pragma (currently?) only has effect in AOT and this change will (for the utf8 scanner) make the JIT version slower (probably by the same ~73.6 mio instructions as - at least in AOT - the implicit check is 6 instructions and the explicit one is 3 instructions). As the pragma doesn't work in the StringScanner anyway I expect the change to be somewhat equivalent there. Once the read-allocate-copy dance is also removed from the utf8 scanner I expect the combined result to be positive all around. Update: With https://dart-review.googlesource.com/c/sdk/+/385201 landed I get these changes: Compiling the CFE: ``` instructions:u: -0.4520% +/- 0.0002% (-98470955.29 +/- 42253.40) ``` Scanner benchmark with `--bytes`: ``` msec task-clock:u: -2.1758% +/- 0.2316% (-92.07 +/- 9.80) cycles:u: -2.1941% +/- 0.2283% (-405224983.11 +/- 42160655.88) instructions:u: -3.1049% +/- 0.0000% (-1272360052.95 +/- 706.54) branch-misses:u: 2.4718% +/- 0.5142% (2371345.23 +/- 493257.76) seconds time elapsed: -2.1761% +/- 0.2317% (-0.09 +/- 0.01) seconds user: -2.2071% +/- 0.2308% (-0.09 +/- 0.01) ``` Scanner benchmark with `--string`: ``` msec task-clock:u: -15.0073% +/- 0.2175% (-745.93 +/- 10.81) page-faults:u: -95.0035% +/- 0.0003% (-228024.25 +/- 0.81) cycles:u: -7.7986% +/- 0.2329% (-1558985588.99 +/- 46560962.79) instructions:u: -3.7054% +/- 0.0000% (-1581977447.66 +/- 481.68) branch-misses:u: -0.6880% +/- 0.5818% (-689453.22 +/- 583101.50) seconds time elapsed: -15.0198% +/- 0.2170% (-0.75 +/- 0.01) seconds user: -8.8149% +/- 0.2648% (-0.41 +/- 0.01) seconds sys: -94.1247% +/- 1.6444% (-0.34 +/- 0.01) MarkSweep( old space) goes from 6 to 0 ``` Change-Id: I524a21f488da7df5dc9d2cdf40112b84896ad3e0 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/383324 Reviewed-by: Brian Wilkerson <[email protected]> Reviewed-by: Johnni Winther <[email protected]> Commit-Queue: Jens Johansen <[email protected]>
1 parent 23f0c0b commit c242d3a

File tree

2 files changed

+70
-44
lines changed

2 files changed

+70
-44
lines changed

pkg/_fe_analyzer_shared/lib/src/scanner/string_scanner.dart

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,24 @@ import 'error_token.dart' show ErrorToken;
3535
*/
3636
class StringScanner extends AbstractScanner {
3737
/** The file content. */
38-
final String string;
38+
final String _string;
39+
final int _stringLengthMinusOne;
3940

40-
/** The current offset in [string]. */
41+
/** The current offset in [_string]. */
4142
@override
4243
int scanOffset = -1;
4344

44-
StringScanner(String string,
45+
StringScanner(this._string,
4546
{ScannerConfiguration? configuration,
4647
bool includeComments = false,
4748
LanguageVersionChanged? languageVersionChanged})
48-
: string = ensureZeroTermination(string),
49+
: _stringLengthMinusOne = _string.length - 1,
4950
super(configuration, includeComments, languageVersionChanged,
50-
numberOfBytesHint: string.length);
51+
numberOfBytesHint: _string.length);
5152

5253
StringScanner.recoveryOptionScanner(StringScanner super.copyFrom)
53-
: string = copyFrom.string,
54+
: _string = copyFrom._string,
55+
_stringLengthMinusOne = copyFrom._stringLengthMinusOne,
5456
scanOffset = copyFrom.scanOffset,
5557
super.recoveryOptionScanner();
5658

@@ -59,23 +61,28 @@ class StringScanner extends AbstractScanner {
5961
return new StringScanner.recoveryOptionScanner(this);
6062
}
6163

62-
static String ensureZeroTermination(String string) {
63-
return (string.isEmpty || string.codeUnitAt(string.length - 1) != 0)
64-
// TODO(lry): abort instead of copying the array, or warn?
65-
? string + '\x00'
66-
: string;
67-
}
68-
6964
static bool isLegalIdentifier(String identifier) {
7065
StringScanner scanner = new StringScanner(identifier);
7166
Token startToken = scanner.tokenize();
7267
return startToken is! ErrorToken && startToken.next!.isEof;
7368
}
7469

7570
@override
76-
int advance() => string.codeUnitAt(++scanOffset);
71+
@pragma('vm:unsafe:no-bounds-checks')
72+
int advance() {
73+
// Always increment so scanOffset goes past the end.
74+
++scanOffset;
75+
if (scanOffset > _stringLengthMinusOne) return 0;
76+
return _string.codeUnitAt(scanOffset);
77+
}
78+
7779
@override
78-
int peek() => string.codeUnitAt(scanOffset + 1);
80+
@pragma('vm:unsafe:no-bounds-checks')
81+
int peek() {
82+
int next = scanOffset + 1;
83+
if (next > _stringLengthMinusOne) return 0;
84+
return _string.codeUnitAt(next);
85+
}
7986

8087
@override
8188
int get stringOffset => scanOffset;
@@ -90,7 +97,7 @@ class StringScanner extends AbstractScanner {
9097
analyzer.StringToken createSubstringToken(TokenType type, int start,
9198
bool asciiOnly, int extraOffset, bool allowLazy) {
9299
return new StringTokenImpl.fromSubstring(
93-
type, string, start, scanOffset + extraOffset, tokenStart,
100+
type, _string, start, scanOffset + extraOffset, tokenStart,
94101
canonicalize: true,
95102
precedingComments: comments,
96103
allowLazyFoo: allowLazy);
@@ -100,9 +107,9 @@ class StringScanner extends AbstractScanner {
100107
analyzer.StringToken createSyntheticSubstringToken(
101108
TokenType type, int start, bool asciiOnly, String syntheticChars) {
102109
String value = syntheticChars.length == 0
103-
? canonicalizeSubString(string, start, scanOffset)
110+
? canonicalizeSubString(_string, start, scanOffset)
104111
: canonicalizeString(
105-
string.substring(start, scanOffset) + syntheticChars);
112+
_string.substring(start, scanOffset) + syntheticChars);
106113
return new SyntheticStringToken(
107114
type, value, tokenStart, value.length - syntheticChars.length);
108115
}
@@ -111,26 +118,29 @@ class StringScanner extends AbstractScanner {
111118
CommentToken createCommentToken(TokenType type, int start, bool asciiOnly,
112119
[int extraOffset = 0]) {
113120
return new CommentTokenImpl.fromSubstring(
114-
type, string, start, scanOffset + extraOffset, tokenStart,
121+
type, _string, start, scanOffset + extraOffset, tokenStart,
115122
canonicalize: true);
116123
}
117124

118125
@override
119126
DartDocToken createDartDocToken(TokenType type, int start, bool asciiOnly,
120127
[int extraOffset = 0]) {
121128
return new DartDocToken.fromSubstring(
122-
type, string, start, scanOffset + extraOffset, tokenStart,
129+
type, _string, start, scanOffset + extraOffset, tokenStart,
123130
canonicalize: true);
124131
}
125132

126133
@override
127134
LanguageVersionToken createLanguageVersionToken(
128135
int start, int major, int minor) {
129136
return new LanguageVersionTokenImpl.fromSubstring(
130-
string, start, scanOffset, tokenStart, major, minor,
137+
_string, start, scanOffset, tokenStart, major, minor,
131138
canonicalize: true);
132139
}
133140

134141
@override
135-
bool atEndOfFile() => scanOffset >= string.length - 1;
142+
// To preserve old behavior we only return true once advance has been out of
143+
// bounds. This should probably change. It's at least used in tests
144+
// (where the eof token has its offset reduced by one to 'fix' this.)
145+
bool atEndOfFile() => scanOffset > _stringLengthMinusOne;
136146
}

pkg/_fe_analyzer_shared/lib/src/scanner/utf8_bytes_scanner.dart

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ class Utf8BytesScanner extends AbstractScanner {
3838
*
3939
* The content is zero-terminated.
4040
*/
41-
final Uint8List bytes;
41+
final Uint8List _bytes;
42+
final int _bytesLengthMinusOne;
4243

4344
/**
4445
* Points to the offset of the last byte returned by [advance].
@@ -94,15 +95,16 @@ class Utf8BytesScanner extends AbstractScanner {
9495
* array whose last element is '0' to signal the end of the file. If this
9596
* is not the case, the entire array is copied before scanning.
9697
*/
97-
Utf8BytesScanner(this.bytes,
98+
Utf8BytesScanner(this._bytes,
9899
{ScannerConfiguration? configuration,
99100
bool includeComments = false,
100101
LanguageVersionChanged? languageVersionChanged,
101102
bool allowLazyStrings = true})
102-
: super(configuration, includeComments, languageVersionChanged,
103-
numberOfBytesHint: bytes.length,
103+
: _bytesLengthMinusOne = _bytes.length - 1,
104+
super(configuration, includeComments, languageVersionChanged,
105+
numberOfBytesHint: _bytes.length,
104106
allowLazyStrings: allowLazyStrings) {
105-
assert(bytes.last == 0);
107+
assert(_bytes.last == 0);
106108
// Skip a leading BOM.
107109
if (containsBomAt(/* offset = */ 0)) {
108110
byteOffset += 3;
@@ -111,7 +113,8 @@ class Utf8BytesScanner extends AbstractScanner {
111113
}
112114

113115
Utf8BytesScanner.createRecoveryOptionScanner(Utf8BytesScanner copyFrom)
114-
: bytes = copyFrom.bytes,
116+
: _bytes = copyFrom._bytes,
117+
_bytesLengthMinusOne = copyFrom._bytesLengthMinusOne,
115118
super.recoveryOptionScanner(copyFrom) {
116119
this.byteOffset = copyFrom.byteOffset;
117120
this.scanSlack = copyFrom.scanSlack;
@@ -127,17 +130,28 @@ class Utf8BytesScanner extends AbstractScanner {
127130
bool containsBomAt(int offset) {
128131
const List<int> BOM_UTF8 = const [0xEF, 0xBB, 0xBF];
129132

130-
return offset + 3 < bytes.length &&
131-
bytes[offset] == BOM_UTF8[0] &&
132-
bytes[offset + 1] == BOM_UTF8[1] &&
133-
bytes[offset + 2] == BOM_UTF8[2];
133+
return offset + 3 < _bytes.length &&
134+
_bytes[offset] == BOM_UTF8[0] &&
135+
_bytes[offset + 1] == BOM_UTF8[1] &&
136+
_bytes[offset + 2] == BOM_UTF8[2];
134137
}
135138

136139
@override
137-
int advance() => bytes[++byteOffset];
140+
@pragma('vm:unsafe:no-bounds-checks')
141+
int advance() {
142+
// Always increment so byteOffset goes past the end.
143+
++byteOffset;
144+
if (byteOffset > _bytesLengthMinusOne) return 0;
145+
return _bytes[byteOffset];
146+
}
138147

139148
@override
140-
int peek() => bytes[byteOffset + 1];
149+
@pragma('vm:unsafe:no-bounds-checks')
150+
int peek() {
151+
int next = byteOffset + 1;
152+
if (next > _bytesLengthMinusOne) return 0;
153+
return _bytes[next];
154+
}
141155

142156
/// Returns the unicode code point starting at the byte offset [startOffset]
143157
/// with the byte [nextByte].
@@ -154,9 +168,10 @@ class Utf8BytesScanner extends AbstractScanner {
154168
} else {
155169
expectedHighBytes = 1; // Bad code unit.
156170
}
171+
// TODO(jensj): Don't we need a bounds check here? Can't I crash this?
157172
int numBytes = 0;
158173
for (int i = 0; i < expectedHighBytes; i++) {
159-
if (bytes[byteOffset + i] < 0x80) {
174+
if (_bytes[byteOffset + i] < 0x80) {
160175
break;
161176
}
162177
numBytes++;
@@ -169,7 +184,7 @@ class Utf8BytesScanner extends AbstractScanner {
169184
// TODO(lry): measurably slow, decode creates first a Utf8Decoder and a
170185
// _Utf8Decoder instance. Also the sublist is eagerly allocated.
171186
String codePoint =
172-
utf8.decode(bytes.sublist(startOffset, end), allowMalformed: true);
187+
utf8.decode(_bytes.sublist(startOffset, end), allowMalformed: true);
173188
if (codePoint.length == 0) {
174189
// The UTF-8 decoder discards leading BOM characters.
175190
// TODO(floitsch): don't just assume that removed characters were the
@@ -214,7 +229,7 @@ class Utf8BytesScanner extends AbstractScanner {
214229
int end = byteOffset;
215230
// TODO(lry): this measurably slows down the scanner for files with unicode.
216231
String s =
217-
utf8.decode(bytes.sublist(startScanOffset, end), allowMalformed: true);
232+
utf8.decode(_bytes.sublist(startScanOffset, end), allowMalformed: true);
218233
utf8Slack += (end - startScanOffset) - s.length;
219234
}
220235

@@ -246,17 +261,18 @@ class Utf8BytesScanner extends AbstractScanner {
246261
analyzer.StringToken createSubstringToken(TokenType type, int start,
247262
bool asciiOnly, int extraOffset, bool allowLazy) {
248263
return new StringTokenImpl.fromUtf8Bytes(
249-
type, bytes, start, byteOffset + extraOffset, asciiOnly, tokenStart,
264+
type, _bytes, start, byteOffset + extraOffset, asciiOnly, tokenStart,
250265
precedingComments: comments, allowLazyFoo: allowLazy);
251266
}
252267

253268
@override
254269
analyzer.StringToken createSyntheticSubstringToken(
255270
TokenType type, int start, bool asciiOnly, String syntheticChars) {
256271
String value = syntheticChars.length == 0
257-
? canonicalizeUtf8SubString(bytes, start, byteOffset, asciiOnly)
272+
? canonicalizeUtf8SubString(_bytes, start, byteOffset, asciiOnly)
258273
: canonicalizeString(
259-
decodeString(bytes, start, byteOffset, asciiOnly) + syntheticChars);
274+
decodeString(_bytes, start, byteOffset, asciiOnly) +
275+
syntheticChars);
260276
return new SyntheticStringToken(
261277
type, value, tokenStart, value.length - syntheticChars.length);
262278
}
@@ -266,23 +282,23 @@ class Utf8BytesScanner extends AbstractScanner {
266282
TokenType type, int start, bool asciiOnly,
267283
[int extraOffset = 0]) {
268284
return new CommentTokenImpl.fromUtf8Bytes(
269-
type, bytes, start, byteOffset + extraOffset, asciiOnly, tokenStart);
285+
type, _bytes, start, byteOffset + extraOffset, asciiOnly, tokenStart);
270286
}
271287

272288
@override
273289
DartDocToken createDartDocToken(TokenType type, int start, bool asciiOnly,
274290
[int extraOffset = 0]) {
275291
return new DartDocToken.fromUtf8Bytes(
276-
type, bytes, start, byteOffset + extraOffset, asciiOnly, tokenStart);
292+
type, _bytes, start, byteOffset + extraOffset, asciiOnly, tokenStart);
277293
}
278294

279295
@override
280296
LanguageVersionToken createLanguageVersionToken(
281297
int start, int major, int minor) {
282298
return new LanguageVersionTokenImpl.fromUtf8Bytes(
283-
bytes, start, byteOffset, tokenStart, major, minor);
299+
_bytes, start, byteOffset, tokenStart, major, minor);
284300
}
285301

286302
@override
287-
bool atEndOfFile() => byteOffset >= bytes.length - 1;
303+
bool atEndOfFile() => byteOffset >= _bytesLengthMinusOne;
288304
}

0 commit comments

Comments
 (0)