diff --git a/benchmarks/bin/binary_decode_packed.dart b/benchmarks/bin/binary_decode_packed.dart new file mode 100644 index 000000000..5e4d3e359 --- /dev/null +++ b/benchmarks/bin/binary_decode_packed.dart @@ -0,0 +1,173 @@ +// Copyright (c) 2025, 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. + +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:fixnum/fixnum.dart'; +import 'package:protobuf_benchmarks/benchmark_base.dart'; +import 'package:protobuf_benchmarks/generated/packed_fields.pb.dart'; + +PackedFields? sink; + +class PackedInt32DecodingBenchmark extends BenchmarkBase { + late final Uint8List encoded; + + PackedInt32DecodingBenchmark() : super('PackedInt32Decoding') { + final rand = Random(123); + final message = PackedFields(); + for (var i = 0; i < 1000000; i += 1) { + message.packedInt32.add(rand.nextInt(2147483647)); + } + encoded = message.writeToBuffer(); + } + + @override + void run() { + sink = PackedFields()..mergeFromBuffer(encoded); + } +} + +class PackedInt64DecodingBenchmark extends BenchmarkBase { + late final Uint8List encoded; + + PackedInt64DecodingBenchmark() : super('PackedInt64Decoding') { + final rand = Random(123); + final message = PackedFields(); + for (var i = 0; i < 1000000; i += 1) { + // Note: `Random` cannot generate more than the number below. + message.packedInt64.add(Int64(rand.nextInt(4294967296))); + } + encoded = message.writeToBuffer(); + } + + @override + void run() { + sink = PackedFields()..mergeFromBuffer(encoded); + } +} + +class PackedUint32DecodingBenchmark extends BenchmarkBase { + late final Uint8List encoded; + + PackedUint32DecodingBenchmark() : super('PackedUint32Decoding') { + final rand = Random(123); + final message = PackedFields(); + for (var i = 0; i < 1000000; i += 1) { + message.packedUint32.add(rand.nextInt(4294967295)); + } + encoded = message.writeToBuffer(); + } + + @override + void run() { + sink = PackedFields()..mergeFromBuffer(encoded); + } +} + +class PackedUint64DecodingBenchmark extends BenchmarkBase { + late final Uint8List encoded; + + PackedUint64DecodingBenchmark() : super('PackedUint64Decoding') { + final rand = Random(123); + final message = PackedFields(); + for (var i = 0; i < 1000000; i += 1) { + // Note: `Random` cannot generate more than the number below. + message.packedUint64.add(Int64(rand.nextInt(4294967296))); + } + encoded = message.writeToBuffer(); + } + + @override + void run() { + sink = PackedFields()..mergeFromBuffer(encoded); + } +} + +class PackedSint32DecodingBenchmark extends BenchmarkBase { + late final Uint8List encoded; + + PackedSint32DecodingBenchmark() : super('PackedSint32Decoding') { + final rand = Random(123); + final message = PackedFields(); + for (var i = 0; i < 1000000; i += 1) { + message.packedSint32.add(rand.nextInt(2147483647)); + } + encoded = message.writeToBuffer(); + } + + @override + void run() { + sink = PackedFields()..mergeFromBuffer(encoded); + } +} + +class PackedSint64DecodingBenchmark extends BenchmarkBase { + late final Uint8List encoded; + + PackedSint64DecodingBenchmark() : super('PackedSint64Decoding') { + final rand = Random(123); + final message = PackedFields(); + for (var i = 0; i < 1000000; i += 1) { + // Note: `Random` cannot generate more than the number below. + message.packedSint64.add(Int64(rand.nextInt(4294967296))); + } + encoded = message.writeToBuffer(); + } + + @override + void run() { + sink = PackedFields()..mergeFromBuffer(encoded); + } +} + +class PackedBoolDecodingBenchmark extends BenchmarkBase { + late final Uint8List encoded; + + PackedBoolDecodingBenchmark() : super('PackedBoolDecoding') { + final rand = Random(123); + final message = PackedFields(); + for (var i = 0; i < 1000000; i += 1) { + message.packedBool.add(rand.nextBool()); + } + encoded = message.writeToBuffer(); + } + + @override + void run() { + sink = PackedFields()..mergeFromBuffer(encoded); + } +} + +class PackedEnumDecodingBenchmark extends BenchmarkBase { + late final Uint8List encoded; + + PackedEnumDecodingBenchmark() : super('PackedEnumDecoding') { + final rand = Random(123); + final message = PackedFields(); + final numEnums = Enum.values.length; + for (var i = 0; i < 1000000; i += 1) { + message.packedEnum.add(Enum.values[rand.nextInt(numEnums)]); + } + encoded = message.writeToBuffer(); + } + + @override + void run() { + sink = PackedFields()..mergeFromBuffer(encoded); + } +} + +void main() { + PackedInt32DecodingBenchmark().report(); + PackedInt64DecodingBenchmark().report(); + PackedUint32DecodingBenchmark().report(); + PackedUint64DecodingBenchmark().report(); + PackedSint32DecodingBenchmark().report(); + PackedSint64DecodingBenchmark().report(); + PackedBoolDecodingBenchmark().report(); + PackedEnumDecodingBenchmark().report(); + + if (int.parse('1') == 0) print(sink); +} diff --git a/benchmarks/protos/packed_fields.proto b/benchmarks/protos/packed_fields.proto new file mode 100644 index 000000000..2e4f94512 --- /dev/null +++ b/benchmarks/protos/packed_fields.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +message PackedFields { + repeated int32 packedInt32 = 1 [packed = true]; + repeated int64 packedInt64 = 2 [packed = true]; + repeated uint32 packedUint32 = 3 [packed = true]; + repeated uint64 packedUint64 = 4 [packed = true]; + repeated sint32 packedSint32 = 5 [packed = true]; + repeated sint64 packedSint64 = 6 [packed = true]; + repeated bool packedBool = 7 [packed = true]; + repeated Enum packedEnum = 8 [packed = true]; +} + +enum Enum { + ENUM_1 = 0; + ENUM_2 = 1; + ENUM_3 = 2; + ENUM_4 = 4; + ENUM_5 = 5; +} diff --git a/benchmarks/tool/compile_protos.sh b/benchmarks/tool/compile_protos.sh index 5fd12b833..729fe9b5e 100755 --- a/benchmarks/tool/compile_protos.sh +++ b/benchmarks/tool/compile_protos.sh @@ -12,6 +12,7 @@ SIMPLE_PROTOS=( "protos/google_message1_proto2.proto" "protos/google_message1_proto3.proto" "protos/google_message2.proto" + "protos/packed_fields.proto" ) set -x diff --git a/protobuf/CHANGELOG.md b/protobuf/CHANGELOG.md index b0bdcb1ac..e06317ac1 100644 --- a/protobuf/CHANGELOG.md +++ b/protobuf/CHANGELOG.md @@ -1,3 +1,9 @@ +## 4.0.1-wip + +* Improve packed field decoding performance. ([#959]) + +[#959]: https://github.com/google/protobuf.dart/pull/959 + ## 4.0.0 * **Breaking:** The following types and members are now removed: diff --git a/protobuf/lib/src/protobuf/coded_buffer.dart b/protobuf/lib/src/protobuf/coded_buffer.dart index 1bcb864ff..463b22b5b 100644 --- a/protobuf/lib/src/protobuf/coded_buffer.dart +++ b/protobuf/lib/src/protobuf/coded_buffer.dart @@ -129,33 +129,65 @@ void _mergeFromCodedBufferReader(BuilderInfo meta, _FieldSet fs, case PbFieldType._REPEATED_BOOL: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readBool())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + // No need to check the element as for `bool` fields we only need to + // check that the value is not null, and we know in `add` below that + // the value isn't null (`readBool` doesn't return `null`). + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readBool()); + } + }); + } } else { - list.add(input.readBool()); + list._checkModifiable('add'); + list._addUnchecked(input.readBool()); } break; case PbFieldType._REPEATED_BYTES: final list = fs._ensureRepeatedField(meta, fi); - list.add(input.readBytes()); + list._checkModifiable('add'); + list._addUnchecked(input.readBytes()); break; case PbFieldType._REPEATED_STRING: final list = fs._ensureRepeatedField(meta, fi); - list.add(input.readString()); + list._checkModifiable('add'); + list._addUnchecked(input.readString()); break; case PbFieldType._REPEATED_FLOAT: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readFloat())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readFloat()); + } + }); + } } else { - list.add(input.readFloat()); + list._checkModifiable('add'); + list._addUnchecked(input.readFloat()); } break; case PbFieldType._REPEATED_DOUBLE: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readDouble())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readDouble()); + } + }); + } } else { - list.add(input.readDouble()); + list._checkModifiable('add'); + list._addUnchecked(input.readDouble()); } break; case PbFieldType._REPEATED_ENUM: @@ -172,81 +204,171 @@ void _mergeFromCodedBufferReader(BuilderInfo meta, _FieldSet fs, case PbFieldType._REPEATED_INT32: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readInt32())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readInt32()); + } + }); + } } else { - list.add(input.readInt32()); + list._checkModifiable('add'); + list._addUnchecked(input.readInt32()); } break; case PbFieldType._REPEATED_INT64: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readInt64())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readInt64()); + } + }); + } } else { - list.add(input.readInt64()); + list._checkModifiable('add'); + list._addUnchecked(input.readInt64()); } break; case PbFieldType._REPEATED_SINT32: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readSint32())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readSint32()); + } + }); + } } else { - list.add(input.readSint32()); + list._checkModifiable('add'); + list._addUnchecked(input.readSint32()); } break; case PbFieldType._REPEATED_SINT64: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readSint64())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readSint64()); + } + }); + } } else { - list.add(input.readSint64()); + list._checkModifiable('add'); + list._addUnchecked(input.readSint64()); } break; case PbFieldType._REPEATED_UINT32: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readUint32())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readUint32()); + } + }); + } } else { - list.add(input.readUint32()); + list._checkModifiable('add'); + list._addUnchecked(input.readUint32()); } break; case PbFieldType._REPEATED_UINT64: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readUint64())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readUint64()); + } + }); + } } else { - list.add(input.readUint64()); + list._checkModifiable('add'); + list._addUnchecked(input.readUint64()); } break; case PbFieldType._REPEATED_FIXED32: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readFixed32())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readFixed32()); + } + }); + } } else { - list.add(input.readFixed32()); + list._checkModifiable('add'); + list._addUnchecked(input.readFixed32()); } break; case PbFieldType._REPEATED_FIXED64: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readFixed64())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readFixed64()); + } + }); + } } else { - list.add(input.readFixed64()); + list._checkModifiable('add'); + list._addUnchecked(input.readFixed64()); } break; case PbFieldType._REPEATED_SFIXED32: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readSfixed32())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readSfixed32()); + } + }); + } } else { - list.add(input.readSfixed32()); + list._checkModifiable('add'); + list._addUnchecked(input.readSfixed32()); } break; case PbFieldType._REPEATED_SFIXED64: final list = fs._ensureRepeatedField(meta, fi); if (wireType == WIRETYPE_LENGTH_DELIMITED) { - _readPacked(input, () => list.add(input.readSfixed64())); + final limit = input.readInt32(); + if (limit != 0) { + list._checkModifiable('add'); + input._withLimit(limit, () { + while (!input.isAtEnd()) { + list._addUnchecked(input.readSfixed64()); + } + }); + } } else { - list.add(input.readSfixed64()); + list._checkModifiable('add'); + list._addUnchecked(input.readSfixed64()); } break; case PbFieldType._REPEATED_MESSAGE: @@ -268,14 +390,6 @@ void _mergeFromCodedBufferReader(BuilderInfo meta, _FieldSet fs, } } -void _readPacked(CodedBufferReader input, void Function() readFunc) { - input._withLimit(input.readInt32(), () { - while (!input.isAtEnd()) { - readFunc(); - } - }); -} - void _readPackableToListEnum( List list, BuilderInfo meta, diff --git a/protobuf/lib/src/protobuf/coded_buffer_reader.dart b/protobuf/lib/src/protobuf/coded_buffer_reader.dart index cc7e3ddf1..e03e4c2c5 100644 --- a/protobuf/lib/src/protobuf/coded_buffer_reader.dart +++ b/protobuf/lib/src/protobuf/coded_buffer_reader.dart @@ -42,14 +42,20 @@ class CodedBufferReader { throw InvalidProtocolBufferException.truncatedMessage(); } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') void checkLastTagWas(int value) { if (_lastTag != value) { throw InvalidProtocolBufferException.invalidEndTag(); } } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') bool isAtEnd() => _bufferPos >= _currentLimit; + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') void _withLimit(int byteLimit, Function() callback) { if (byteLimit < 0) { throw ArgumentError( @@ -66,6 +72,8 @@ class CodedBufferReader { _currentLimit = oldLimit; } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') void _checkLimit(int increment) { assert(_currentLimit != -1); _bufferPos += increment; @@ -121,28 +129,56 @@ class CodedBufferReader { _currentLimit = oldLimit; } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') int readEnum() => readInt32(); + + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') int readInt32() => _readRawVarint32(true); + + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') Int64 readInt64() => _readRawVarint64(); + + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') int readUint32() => _readRawVarint32(false); + + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') Int64 readUint64() => _readRawVarint64(); + + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') int readSint32() => _decodeZigZag32(readUint32()); + + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') Int64 readSint64() => _decodeZigZag64(readUint64()); + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') int readFixed32() { final pos = _bufferPos; _checkLimit(4); return _byteData.getUint32(pos, Endian.little); } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') Int64 readFixed64() => readSfixed64(); + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') int readSfixed32() { final pos = _bufferPos; _checkLimit(4); return _byteData.getInt32(pos, Endian.little); } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') Int64 readSfixed64() { final pos = _bufferPos; _checkLimit(8); @@ -150,15 +186,21 @@ class CodedBufferReader { return Int64.fromBytes(view); } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') bool readBool() => _readRawVarint32(true) != 0; /// Read a length-delimited field as bytes. + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') Uint8List readBytes() => Uint8List.fromList(readBytesAsView()); /// Read a length-delimited field as a view of the [CodedBufferReader]'s /// buffer. When storing the returned value directly (instead of e.g. parsing /// it as a UTF-8 string and copying) use [readBytes] instead to avoid /// holding on to the whole message, or copy the returned view. + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') Uint8List readBytesAsView() { final length = readInt32(); _checkLimit(length); @@ -166,6 +208,8 @@ class CodedBufferReader { _buffer.buffer, _buffer.offsetInBytes + _bufferPos - length, length); } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') String readString() { final length = readInt32(); final stringPos = _bufferPos; @@ -174,18 +218,24 @@ class CodedBufferReader { .convert(_buffer, stringPos, stringPos + length); } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') double readFloat() { final pos = _bufferPos; _checkLimit(4); return _byteData.getFloat32(pos, Endian.little); } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') double readDouble() { final pos = _bufferPos; _checkLimit(8); return _byteData.getFloat64(pos, Endian.little); } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') int readTag() { if (isAtEnd()) { _lastTag = 0; @@ -228,6 +278,8 @@ class CodedBufferReader { } } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') static int _decodeZigZag32(int value) { if ((value & 0x1) == 1) { return -(value >> 1) - 1; @@ -236,11 +288,15 @@ class CodedBufferReader { } } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') static Int64 _decodeZigZag64(Int64 value) { if ((value & 0x1) == 1) value = -value; return value >> 1; } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') int _readRawVarintByte() { _checkLimit(1); return _buffer[_bufferPos - 1]; diff --git a/protobuf/lib/src/protobuf/extension_field_set.dart b/protobuf/lib/src/protobuf/extension_field_set.dart index f90bb9096..9e9b5b010 100644 --- a/protobuf/lib/src/protobuf/extension_field_set.dart +++ b/protobuf/lib/src/protobuf/extension_field_set.dart @@ -66,6 +66,8 @@ class _ExtensionFieldSet { return newList; } + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') dynamic _getFieldOrNull(Extension extension) => _values[extension.tagNumber]; void _clearFieldAndInfo(Extension fi) { diff --git a/protobuf/lib/src/protobuf/field_set.dart b/protobuf/lib/src/protobuf/field_set.dart index ea3dac841..780a8fe7b 100644 --- a/protobuf/lib/src/protobuf/field_set.dart +++ b/protobuf/lib/src/protobuf/field_set.dart @@ -4,6 +4,8 @@ part of '../../protobuf.dart'; +@pragma('vm:never-inline') +@pragma('wasm:never-inline') void _throwFrozenMessageModificationError(String messageName, [String? methodName]) { if (methodName != null) { diff --git a/protobuf/lib/src/protobuf/pb_list.dart b/protobuf/lib/src/protobuf/pb_list.dart index 792bfff4e..0e0ccc1de 100644 --- a/protobuf/lib/src/protobuf/pb_list.dart +++ b/protobuf/lib/src/protobuf/pb_list.dart @@ -52,6 +52,13 @@ class PbList extends ListBase { _wrappedList.add(element); } + @pragma('dart2js:tryInline') + @pragma('vm:prefer-inline') + @pragma('wasm:prefer-inline') + void _addUnchecked(E element) { + _wrappedList.add(element); + } + @override @pragma('dart2js:never-inline') void addAll(Iterable iterable) { diff --git a/protobuf/pubspec.yaml b/protobuf/pubspec.yaml index af09c1a4b..043f8bad0 100644 --- a/protobuf/pubspec.yaml +++ b/protobuf/pubspec.yaml @@ -1,5 +1,5 @@ name: protobuf -version: 4.0.0 +version: 4.0.1-wip description: >- Runtime library for protocol buffers support. Use with package:protoc_plugin to generate Dart code for your '.proto' files.