Skip to content

Commit 8944349

Browse files
javachemeta-codesync[bot]
authored andcommitted
Optimize MapBuffer representation (#57361)
Summary: Pull Request resolved: #57361 Broadens the former header-shrink change into a single representation-optimization commit for MapBuffer. It bundles three layout optimizations that were previously split: (1) the header is reduced to a single 2-byte `count` field; (2) multi-byte values are read via `memcpy` so unaligned access is well-defined on all platforms; (3) every dynamic-data entry (`String`, `Map`, `MapBufferList`, `IntBuffer`, `DoubleBuffer`) packs its `[offset][byteLength]` into the bucket's 8-byte value instead of writing an in-band length prefix into the dynamic data section. Net effect: 4 fewer bytes per dynamic entry, one fewer indirection on read (the length is already in the bucket), and every dynamic entry becomes self-delimiting from its bucket alone. No public API change — only the internal serialized representation. Changelog: [Internal] Reviewed By: zeyap Differential Revision: D109848478
1 parent c9d5abb commit 8944349

14 files changed

Lines changed: 165 additions & 177 deletions

File tree

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.kt

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,11 @@ private constructor(
5151
ReadableMapBuffer(buffer.duplicate().apply { position(offset) }, offset)
5252

5353
private fun readHeader() {
54-
// byte order
55-
val storedAlignment = buffer.short
56-
if (storedAlignment.toInt() != ALIGNMENT) {
57-
buffer.order(ByteOrder.LITTLE_ENDIAN)
58-
}
59-
// count
60-
count = readUnsignedShort(buffer.position()).toInt()
54+
// The C++ writer always serializes in little-endian byte order. ByteBuffer
55+
// defaults to big-endian and duplicate() resets the order, so set it
56+
// explicitly on every instance, including nested clones.
57+
buffer.order(ByteOrder.LITTLE_ENDIAN)
58+
count = readUnsignedShort(offsetToMapBuffer).toInt()
6159
}
6260

6361
/**
@@ -122,26 +120,27 @@ private constructor(
122120
return readIntValue(bufferPosition) == 1
123121
}
124122

123+
// Dynamic-data entries store [offset][byteLength] in the bucket's 8-byte
124+
// value: getInt(bufferPosition) is the offset, getInt(bufferPosition + 4) is
125+
// the byte length. The dynamic data section itself carries no length prefix.
125126
private fun readStringValue(bufferPosition: Int): String {
126127
val offset = offsetForDynamicData + buffer.getInt(bufferPosition)
127-
val sizeOfString = buffer.getInt(offset)
128+
val sizeOfString = buffer.getInt(bufferPosition + Int.SIZE_BYTES)
128129
val result = ByteArray(sizeOfString)
129-
val stringOffset = offset + Int.SIZE_BYTES
130-
buffer.position(stringOffset)
130+
buffer.position(offset)
131131
buffer[result, 0, sizeOfString]
132132
return String(result)
133133
}
134134

135135
private fun readMapBufferValue(position: Int): ReadableMapBuffer {
136136
val offset = offsetForDynamicData + buffer.getInt(position)
137-
return cloneWithOffset(offset + Int.SIZE_BYTES)
137+
return cloneWithOffset(offset)
138138
}
139139

140140
private fun readMapBufferListValue(position: Int): List<ReadableMapBuffer> {
141141
val readMapBufferList = arrayListOf<ReadableMapBuffer>()
142-
var offset = offsetForDynamicData + buffer.getInt(position)
143-
val sizeMapBufferList = buffer.getInt(offset)
144-
offset += Int.SIZE_BYTES
142+
val offset = offsetForDynamicData + buffer.getInt(position)
143+
val sizeMapBufferList = buffer.getInt(position + Int.SIZE_BYTES)
145144
var curLen = 0
146145
while (curLen < sizeMapBufferList) {
147146
val sizeMapBuffer = buffer.getInt(offset + curLen)
@@ -153,16 +152,16 @@ private constructor(
153152
}
154153

155154
private fun readIntBufferValue(bufferPosition: Int): IntArray {
156-
var offset = offsetForDynamicData + buffer.getInt(bufferPosition)
157-
val count = buffer.getInt(offset)
158-
offset += Int.SIZE_BYTES
155+
val offset = offsetForDynamicData + buffer.getInt(bufferPosition)
156+
val byteLength = buffer.getInt(bufferPosition + Int.SIZE_BYTES)
157+
val count = byteLength / Int.SIZE_BYTES
159158
return IntArray(count) { i -> buffer.getInt(offset + i * Int.SIZE_BYTES) }
160159
}
161160

162161
private fun readDoubleBufferValue(bufferPosition: Int): DoubleArray {
163-
var offset = offsetForDynamicData + buffer.getInt(bufferPosition)
164-
val count = buffer.getInt(offset)
165-
offset += Int.SIZE_BYTES
162+
val offset = offsetForDynamicData + buffer.getInt(bufferPosition)
163+
val byteLength = buffer.getInt(bufferPosition + Int.SIZE_BYTES)
164+
val count = byteLength / Double.SIZE_BYTES
166165
return DoubleArray(count) { i -> buffer.getDouble(offset + i * Double.SIZE_BYTES) }
167166
}
168167

@@ -355,13 +354,10 @@ private constructor(
355354
}
356355

357356
public companion object {
358-
// Value used to verify if the data is serialized with LittleEndian order.
359-
private const val ALIGNMENT = 0xFE
360-
361-
// 8 bytes = 2 (alignment) + 2 (count) + 4 (size)
362-
private const val HEADER_SIZE = 8
357+
// 2 bytes = 2 (count)
358+
private const val HEADER_SIZE = 2
363359

364-
// 10 bytes = 2 (key) + 2 (type) + 8 (value)
360+
// 12 bytes = 2 (key) + 2 (type) + 8 (value)
365361
private const val BUCKET_SIZE = 12
366362

367363
// 2 bytes = 2 (key)

packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp

Lines changed: 75 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,43 @@
88
#include "MapBuffer.h"
99
#include <react/renderer/mapbuffer/MapBufferBuilder.h>
1010

11+
#include <algorithm>
12+
#include <cstring>
13+
1114
namespace facebook::react {
1215

16+
namespace {
17+
// Reads a value of type T from a (possibly unaligned) offset in the buffer.
18+
// MapBuffer's packed layout places multi-byte values at offsets that are not
19+
// naturally aligned for their type (e.g. an 8-byte value at a 2-byte boundary),
20+
// so dereferencing a reinterpret_cast pointer there is undefined behavior and
21+
// can fault on 32-bit ARM. memcpy compiles to a single unaligned load on
22+
// arm64/x86 and to alignment-safe loads on armv7.
23+
template <typename T>
24+
inline T readUnaligned(const uint8_t* data, int32_t offset) {
25+
T value;
26+
std::memcpy(&value, data + offset, sizeof(T));
27+
return value;
28+
}
29+
30+
// Debug-asserts on OOB (catches corrupt buffers early in dev) AND clamps in
31+
// release so a corrupt bucket length can never drive an OOB memcpy read.
32+
// react_native_assert is compiled out in release, so the runtime cost outside
33+
// dev is the single min() call.
34+
inline int32_t
35+
clampToBufferBounds(int32_t offset, int32_t byteLength, size_t bufferSize) {
36+
react_native_assert(offset >= 0 && byteLength >= 0);
37+
react_native_assert(
38+
static_cast<size_t>(offset) + static_cast<size_t>(byteLength) <=
39+
bufferSize);
40+
size_t maxLength = bufferSize > static_cast<size_t>(offset)
41+
? bufferSize - static_cast<size_t>(offset)
42+
: 0;
43+
return static_cast<int32_t>(
44+
std::min(static_cast<size_t>(std::max(byteLength, 0)), maxLength));
45+
}
46+
} // namespace
47+
1348
static inline int32_t bucketOffset(int32_t index) {
1449
return sizeof(MapBuffer::Header) + sizeof(MapBuffer::Bucket) * index;
1550
}
@@ -18,17 +53,19 @@ static inline int32_t valueOffset(int32_t bucketIndex) {
1853
return bucketOffset(bucketIndex) + offsetof(MapBuffer::Bucket, data);
1954
}
2055

56+
// Dynamic-data entries pack [offset (low 32 bits)][byteLength (high 32 bits)]
57+
// into the bucket's 8-byte value, so the payload in the dynamic data section
58+
// carries no in-band length prefix. This returns the position of the high
59+
// 32 bits (the length).
60+
static inline int32_t lengthOffset(int32_t bucketIndex) {
61+
return valueOffset(bucketIndex) + static_cast<int32_t>(sizeof(int32_t));
62+
}
63+
2164
// TODO T83483191: Extend MapBuffer C++ implementation to support basic random
2265
// access
2366
MapBuffer::MapBuffer(std::vector<uint8_t> data) : bytes_(std::move(data)) {
2467
auto header = reinterpret_cast<const Header*>(bytes_.data());
2568
count_ = header->count;
26-
27-
if (header->bufferSize != bytes_.size()) {
28-
LOG(ERROR) << "Error: Data size does not match, expected "
29-
<< header->bufferSize << " found: " << bytes_.size();
30-
abort();
31-
}
3269
}
3370

3471
int32_t MapBuffer::getKeyBucket(Key key) const {
@@ -37,8 +74,7 @@ int32_t MapBuffer::getKeyBucket(Key key) const {
3774
while (lo <= hi) {
3875
int32_t mid = (lo + hi) >> 1;
3976

40-
Key midVal =
41-
*reinterpret_cast<const Key*>(bytes_.data() + bucketOffset(mid));
77+
Key midVal = readUnaligned<Key>(bytes_.data(), bucketOffset(mid));
4278

4379
if (midVal < key) {
4480
lo = mid + 1;
@@ -53,8 +89,7 @@ int32_t MapBuffer::getKeyBucket(Key key) const {
5389
}
5490

5591
inline int32_t MapBuffer::getIntAtBucket(int32_t bucketIndex) const {
56-
return *reinterpret_cast<const int32_t*>(
57-
bytes_.data() + valueOffset(bucketIndex));
92+
return readUnaligned<int32_t>(bytes_.data(), valueOffset(bucketIndex));
5893
}
5994

6095
int32_t MapBuffer::getInt(Key key) const {
@@ -74,8 +109,7 @@ int64_t MapBuffer::getLong(Key key) const {
74109
return 0;
75110
}
76111

77-
return *reinterpret_cast<const int64_t*>(
78-
bytes_.data() + valueOffset(bucketIndex));
112+
return readUnaligned<int64_t>(bytes_.data(), valueOffset(bucketIndex));
79113
}
80114

81115
bool MapBuffer::getBool(Key key) const {
@@ -89,8 +123,7 @@ double MapBuffer::getDouble(Key key) const {
89123
return 0;
90124
}
91125

92-
return *reinterpret_cast<const double*>(
93-
bytes_.data() + valueOffset(bucketIndex));
126+
return readUnaligned<double>(bytes_.data(), valueOffset(bucketIndex));
94127
}
95128

96129
int32_t MapBuffer::getDynamicDataOffset() const {
@@ -107,9 +140,10 @@ std::string MapBuffer::getString(Key key) const {
107140
}
108141

109142
int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex);
110-
int32_t stringLength =
111-
*reinterpret_cast<const int32_t*>(bytes_.data() + offset);
112-
const uint8_t* stringPtr = bytes_.data() + offset + sizeof(int);
143+
auto stringLength =
144+
readUnaligned<int32_t>(bytes_.data(), lengthOffset(bucketIndex));
145+
stringLength = clampToBufferBounds(offset, stringLength, bytes_.size());
146+
const uint8_t* stringPtr = bytes_.data() + offset;
113147

114148
return {stringPtr, stringPtr + stringLength};
115149
}
@@ -122,17 +156,13 @@ MapBuffer MapBuffer::getMapBuffer(Key key) const {
122156
}
123157

124158
int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex);
125-
int32_t mapBufferLength =
126-
*reinterpret_cast<const int32_t*>(bytes_.data() + offset);
127-
size_t maxLength = bytes_.size() - offset - sizeof(int32_t);
128-
if (mapBufferLength > maxLength) {
129-
mapBufferLength = maxLength;
130-
}
159+
auto mapBufferLength =
160+
readUnaligned<int32_t>(bytes_.data(), lengthOffset(bucketIndex));
161+
mapBufferLength = clampToBufferBounds(offset, mapBufferLength, bytes_.size());
131162

132163
std::vector<uint8_t> value(mapBufferLength);
133164

134-
memcpy(
135-
value.data(), bytes_.data() + offset + sizeof(int32_t), mapBufferLength);
165+
memcpy(value.data(), bytes_.data() + offset, mapBufferLength);
136166

137167
return MapBuffer(std::move(value));
138168
}
@@ -146,15 +176,18 @@ std::vector<MapBuffer> MapBuffer::getMapBufferList(MapBuffer::Key key) const {
146176

147177
std::vector<MapBuffer> mapBufferList;
148178
int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex);
149-
int32_t mapBufferListLength =
150-
*reinterpret_cast<const int32_t*>(bytes_.data() + offset);
151-
offset = offset + sizeof(uint32_t);
179+
auto mapBufferListLength =
180+
readUnaligned<int32_t>(bytes_.data(), lengthOffset(bucketIndex));
181+
mapBufferListLength =
182+
clampToBufferBounds(offset, mapBufferListLength, bytes_.size());
152183

153184
int32_t curLen = 0;
154185
while (curLen < mapBufferListLength) {
155-
int32_t mapBufferLength =
156-
*reinterpret_cast<const int32_t*>(bytes_.data() + offset + curLen);
186+
auto mapBufferLength =
187+
readUnaligned<int32_t>(bytes_.data(), offset + curLen);
157188
curLen = curLen + sizeof(uint32_t);
189+
mapBufferLength =
190+
clampToBufferBounds(offset + curLen, mapBufferLength, bytes_.size());
158191
std::vector<uint8_t> value(mapBufferLength);
159192
memcpy(value.data(), bytes_.data() + offset + curLen, mapBufferLength);
160193
mapBufferList.emplace_back(std::move(value));
@@ -171,14 +204,15 @@ std::vector<int32_t> MapBuffer::getIntBuffer(MapBuffer::Key key) const {
171204
}
172205

173206
int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex);
174-
int32_t count = *reinterpret_cast<const int32_t*>(bytes_.data() + offset);
207+
auto byteLength =
208+
readUnaligned<int32_t>(bytes_.data(), lengthOffset(bucketIndex));
209+
byteLength = clampToBufferBounds(offset, byteLength, bytes_.size());
210+
int32_t count = byteLength / static_cast<int32_t>(sizeof(int32_t));
175211

176212
std::vector<int32_t> result(count);
177-
if (count > 0) {
213+
if (byteLength > 0) {
178214
memcpy(
179-
result.data(),
180-
bytes_.data() + offset + sizeof(int32_t),
181-
static_cast<size_t>(count) * sizeof(int32_t));
215+
result.data(), bytes_.data() + offset, static_cast<size_t>(byteLength));
182216
}
183217
return result;
184218
}
@@ -191,14 +225,15 @@ std::vector<double> MapBuffer::getDoubleBuffer(MapBuffer::Key key) const {
191225
}
192226

193227
int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex);
194-
int32_t count = *reinterpret_cast<const int32_t*>(bytes_.data() + offset);
228+
auto byteLength =
229+
readUnaligned<int32_t>(bytes_.data(), lengthOffset(bucketIndex));
230+
byteLength = clampToBufferBounds(offset, byteLength, bytes_.size());
231+
int32_t count = byteLength / static_cast<int32_t>(sizeof(double));
195232

196233
std::vector<double> result(count);
197-
if (count > 0) {
234+
if (byteLength > 0) {
198235
memcpy(
199-
result.data(),
200-
bytes_.data() + offset + sizeof(int32_t),
201-
static_cast<size_t>(count) * sizeof(double));
236+
result.data(), bytes_.data() + offset, static_cast<size_t>(byteLength));
202237
}
203238
return result;
204239
}

packages/react-native/ReactCommon/react/renderer/mapbuffer/MapBuffer.h

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ class JReadableMapBuffer;
4040
*
4141
* MapBuffer data is stored in a continuous chunk of memory (bytes_ field below) with the following layout:
4242
*
43-
* ┌─────────────────────Header──────────────────────┐
44-
* │ 10 bytes
45-
* ├─Alignment─┬─Item count─┬──────Buffer size───────┤
46-
* │ 2 bytes │ 2 bytes │ 4 bytes
47-
* └───────────┴────────────┴────────────────────────┘
43+
* ┌──────Header──────┐
44+
* │ 2 bytes │
45+
* ├────Item count────┤
46+
* │ 2 bytes │
47+
* └──────────────────┘
4848
* ┌────────────────────────────────────────────────────────────────────────────────────────┐
4949
* │ Buckets (one per item in the map) │
5050
* │ │
@@ -69,14 +69,8 @@ class MapBuffer {
6969
public:
7070
using Key = uint16_t;
7171

72-
// The first value in the buffer, used to check correct encoding/endianness on
73-
// JVM side.
74-
constexpr static uint16_t HEADER_ALIGNMENT = 0xFE;
75-
7672
struct Header {
77-
uint16_t alignment = HEADER_ALIGNMENT; // alignment of serialization
7873
uint16_t count; // amount of items in the map
79-
uint32_t bufferSize; // Amount of bytes used to store the map in memory
8074
};
8175

8276
#pragma pack(push, 1)
@@ -89,7 +83,7 @@ class MapBuffer {
8983
};
9084
#pragma pack(pop)
9185

92-
static_assert(sizeof(Header) == 8, "MapBuffer header size is incorrect.");
86+
static_assert(sizeof(Header) == 2, "MapBuffer header size is incorrect.");
9387
static_assert(sizeof(Bucket) == 12, "MapBuffer bucket size is incorrect.");
9488

9589
/**
@@ -112,9 +106,11 @@ class MapBuffer {
112106
// array within the dynamic data section.
113107
IntBuffer = 6,
114108
DoubleBuffer = 7,
115-
// A homogeneous, ordered array of nested MapBuffers. Distinct from `Map` so
116-
// that a list of MapBuffers is self-describing (a single Map and a list are
117-
// byte-distinct in payload but previously shared the `Map` type tag).
109+
// A homogeneous, ordered array of nested MapBuffers. Stored in the dynamic
110+
// data section as [int32 totalSize] followed by a sequence of
111+
// [int32 childSize][child bytes]. Distinct from `Map` so that a list of
112+
// MapBuffers is self-describing (a single Map and a list are byte-distinct
113+
// in payload but previously shared the `Map` type tag).
118114
MapBufferList = 8,
119115
};
120116

0 commit comments

Comments
 (0)