Skip to content

Commit 8218129

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] Differential Revision: D109848478
1 parent 3797ee8 commit 8218129

14 files changed

Lines changed: 138 additions & 172 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: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,25 @@
88
#include "MapBuffer.h"
99
#include <react/renderer/mapbuffer/MapBufferBuilder.h>
1010

11+
#include <cstring>
12+
1113
namespace facebook::react {
1214

15+
namespace {
16+
// Reads a value of type T from a (possibly unaligned) offset in the buffer.
17+
// MapBuffer's packed layout places multi-byte values at offsets that are not
18+
// naturally aligned for their type (e.g. an 8-byte value at a 2-byte boundary),
19+
// so dereferencing a reinterpret_cast pointer there is undefined behavior and
20+
// can fault on 32-bit ARM. memcpy compiles to a single unaligned load on
21+
// arm64/x86 and to alignment-safe loads on armv7.
22+
template <typename T>
23+
inline T readUnaligned(const uint8_t* data, int32_t offset) {
24+
T value;
25+
std::memcpy(&value, data + offset, sizeof(T));
26+
return value;
27+
}
28+
} // namespace
29+
1330
static inline int32_t bucketOffset(int32_t index) {
1431
return sizeof(MapBuffer::Header) + sizeof(MapBuffer::Bucket) * index;
1532
}
@@ -18,17 +35,19 @@ static inline int32_t valueOffset(int32_t bucketIndex) {
1835
return bucketOffset(bucketIndex) + offsetof(MapBuffer::Bucket, data);
1936
}
2037

38+
// Dynamic-data entries pack [offset (low 32 bits)][byteLength (high 32 bits)]
39+
// into the bucket's 8-byte value, so the payload in the dynamic data section
40+
// carries no in-band length prefix. This returns the position of the high
41+
// 32 bits (the length).
42+
static inline int32_t lengthOffset(int32_t bucketIndex) {
43+
return valueOffset(bucketIndex) + static_cast<int32_t>(sizeof(int32_t));
44+
}
45+
2146
// TODO T83483191: Extend MapBuffer C++ implementation to support basic random
2247
// access
2348
MapBuffer::MapBuffer(std::vector<uint8_t> data) : bytes_(std::move(data)) {
2449
auto header = reinterpret_cast<const Header*>(bytes_.data());
2550
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-
}
3251
}
3352

3453
int32_t MapBuffer::getKeyBucket(Key key) const {
@@ -37,8 +56,7 @@ int32_t MapBuffer::getKeyBucket(Key key) const {
3756
while (lo <= hi) {
3857
int32_t mid = (lo + hi) >> 1;
3958

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

4361
if (midVal < key) {
4462
lo = mid + 1;
@@ -53,8 +71,7 @@ int32_t MapBuffer::getKeyBucket(Key key) const {
5371
}
5472

5573
inline int32_t MapBuffer::getIntAtBucket(int32_t bucketIndex) const {
56-
return *reinterpret_cast<const int32_t*>(
57-
bytes_.data() + valueOffset(bucketIndex));
74+
return readUnaligned<int32_t>(bytes_.data(), valueOffset(bucketIndex));
5875
}
5976

6077
int32_t MapBuffer::getInt(Key key) const {
@@ -74,8 +91,7 @@ int64_t MapBuffer::getLong(Key key) const {
7491
return 0;
7592
}
7693

77-
return *reinterpret_cast<const int64_t*>(
78-
bytes_.data() + valueOffset(bucketIndex));
94+
return readUnaligned<int64_t>(bytes_.data(), valueOffset(bucketIndex));
7995
}
8096

8197
bool MapBuffer::getBool(Key key) const {
@@ -89,8 +105,7 @@ double MapBuffer::getDouble(Key key) const {
89105
return 0;
90106
}
91107

92-
return *reinterpret_cast<const double*>(
93-
bytes_.data() + valueOffset(bucketIndex));
108+
return readUnaligned<double>(bytes_.data(), valueOffset(bucketIndex));
94109
}
95110

96111
int32_t MapBuffer::getDynamicDataOffset() const {
@@ -108,8 +123,8 @@ std::string MapBuffer::getString(Key key) const {
108123

109124
int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex);
110125
int32_t stringLength =
111-
*reinterpret_cast<const int32_t*>(bytes_.data() + offset);
112-
const uint8_t* stringPtr = bytes_.data() + offset + sizeof(int);
126+
readUnaligned<int32_t>(bytes_.data(), lengthOffset(bucketIndex));
127+
const uint8_t* stringPtr = bytes_.data() + offset;
113128

114129
return {stringPtr, stringPtr + stringLength};
115130
}
@@ -123,16 +138,15 @@ MapBuffer MapBuffer::getMapBuffer(Key key) const {
123138

124139
int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex);
125140
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;
141+
readUnaligned<int32_t>(bytes_.data(), lengthOffset(bucketIndex));
142+
size_t maxLength = bytes_.size() - offset;
143+
if (static_cast<size_t>(mapBufferLength) > maxLength) {
144+
mapBufferLength = static_cast<int32_t>(maxLength);
130145
}
131146

132147
std::vector<uint8_t> value(mapBufferLength);
133148

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

137151
return MapBuffer(std::move(value));
138152
}
@@ -147,13 +161,12 @@ std::vector<MapBuffer> MapBuffer::getMapBufferList(MapBuffer::Key key) const {
147161
std::vector<MapBuffer> mapBufferList;
148162
int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex);
149163
int32_t mapBufferListLength =
150-
*reinterpret_cast<const int32_t*>(bytes_.data() + offset);
151-
offset = offset + sizeof(uint32_t);
164+
readUnaligned<int32_t>(bytes_.data(), lengthOffset(bucketIndex));
152165

153166
int32_t curLen = 0;
154167
while (curLen < mapBufferListLength) {
155168
int32_t mapBufferLength =
156-
*reinterpret_cast<const int32_t*>(bytes_.data() + offset + curLen);
169+
readUnaligned<int32_t>(bytes_.data(), offset + curLen);
157170
curLen = curLen + sizeof(uint32_t);
158171
std::vector<uint8_t> value(mapBufferLength);
159172
memcpy(value.data(), bytes_.data() + offset + curLen, mapBufferLength);
@@ -171,14 +184,14 @@ std::vector<int32_t> MapBuffer::getIntBuffer(MapBuffer::Key key) const {
171184
}
172185

173186
int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex);
174-
int32_t count = *reinterpret_cast<const int32_t*>(bytes_.data() + offset);
187+
int32_t byteLength =
188+
readUnaligned<int32_t>(bytes_.data(), lengthOffset(bucketIndex));
189+
int32_t count = byteLength / static_cast<int32_t>(sizeof(int32_t));
175190

176191
std::vector<int32_t> result(count);
177-
if (count > 0) {
192+
if (byteLength > 0) {
178193
memcpy(
179-
result.data(),
180-
bytes_.data() + offset + sizeof(int32_t),
181-
static_cast<size_t>(count) * sizeof(int32_t));
194+
result.data(), bytes_.data() + offset, static_cast<size_t>(byteLength));
182195
}
183196
return result;
184197
}
@@ -191,14 +204,14 @@ std::vector<double> MapBuffer::getDoubleBuffer(MapBuffer::Key key) const {
191204
}
192205

193206
int32_t offset = getDynamicDataOffset() + getIntAtBucket(bucketIndex);
194-
int32_t count = *reinterpret_cast<const int32_t*>(bytes_.data() + offset);
207+
int32_t byteLength =
208+
readUnaligned<int32_t>(bytes_.data(), lengthOffset(bucketIndex));
209+
int32_t count = byteLength / static_cast<int32_t>(sizeof(double));
195210

196211
std::vector<double> result(count);
197-
if (count > 0) {
212+
if (byteLength > 0) {
198213
memcpy(
199-
result.data(),
200-
bytes_.data() + offset + sizeof(int32_t),
201-
static_cast<size_t>(count) * sizeof(double));
214+
result.data(), bytes_.data() + offset, static_cast<size_t>(byteLength));
202215
}
203216
return result;
204217
}

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)