Skip to content

Commit c2467b0

Browse files
committed
Increase python performance
- Builder startup work is cheaper: StartObject now seeds vtable state with [0] * numfields, lazy create sharedStrings dict to speed up objects with no strings - Offset/Pad/Prep all work off cached head/buffer lengths and zero-fill via slices - Prepend now handles alignment + byte writes in one pass - Vtable write is batched: WriteVtable gathers all field offsets plus metadata and streams them
1 parent ac8b124 commit c2467b0

File tree

1 file changed

+113
-81
lines changed

1 file changed

+113
-81
lines changed

python/flatbuffers/builder.py

Lines changed: 113 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
from . import packer
2121
from .compat import memoryview_type
2222
from .compat import NumpyRequiredForThisFeature, import_numpy
23-
from .compat import range_func
2423
from .number_types import (SOffsetTFlags, UOffsetTFlags, VOffsetTFlags)
2524

2625
np = import_numpy()
@@ -159,7 +158,7 @@ def __init__(self, initialSize=1024):
159158
self.vtables = {}
160159
self.nested = False
161160
self.forceDefaults = False
162-
self.sharedStrings = {}
161+
self.sharedStrings = None
163162
## @endcond
164163
self.finished = False
165164

@@ -172,7 +171,7 @@ def Clear(self) -> None:
172171
self.vtables = {}
173172
self.nested = False
174173
self.forceDefaults = False
175-
self.sharedStrings = {}
174+
self.sharedStrings = None
176175
self.vectorNumElems = None
177176
## @endcond
178177
self.finished = False
@@ -192,7 +191,7 @@ def Output(self):
192191
if not self.finished:
193192
raise BuilderNotFinishedError()
194193

195-
return self.Bytes[self.Head() :]
194+
return self.Bytes[self.head :]
196195

197196
## @cond FLATBUFFERS_INTERNAL
198197
def StartObject(self, numfields):
@@ -201,7 +200,7 @@ def StartObject(self, numfields):
201200
self.assertNotNested()
202201

203202
# use 32-bit offsets so that arithmetic doesn't overflow.
204-
self.current_vtable = [0 for _ in range_func(numfields)]
203+
self.current_vtable = [0] * numfields
205204
self.objectEnd = self.Offset()
206205
self.nested = True
207206

@@ -258,6 +257,7 @@ def WriteVtable(self):
258257
i = len(self.current_vtable) - 1
259258
trailing = 0
260259
trim = True
260+
vt_entries = []
261261
while i >= 0:
262262
off = 0
263263
elem = self.current_vtable[i]
@@ -273,17 +273,24 @@ def WriteVtable(self):
273273
off = objectOffset - elem
274274
trim = False
275275

276-
self.PrependVOffsetT(off)
276+
vt_entries.append(VOffsetTFlags.py_type(off))
277277

278278
# The two metadata fields are written last.
279279

280280
# First, store the object bytesize:
281-
self.PrependVOffsetT(VOffsetTFlags.py_type(objectSize))
281+
vt_entries.append(VOffsetTFlags.py_type(objectSize))
282282

283283
# Second, store the vtable bytesize:
284284
vBytes = len(self.current_vtable) - trailing + VtableMetadataFields
285285
vBytes *= N.VOffsetTFlags.bytewidth
286-
self.PrependVOffsetT(VOffsetTFlags.py_type(vBytes))
286+
vt_entries.append(VOffsetTFlags.py_type(vBytes))
287+
288+
field_entries = vt_entries[:-2]
289+
field_entries.reverse()
290+
ordered_entries = [vt_entries[-1], vt_entries[-2]]
291+
ordered_entries.extend(field_entries)
292+
293+
self.WriteVtableEntries(ordered_entries)
287294

288295
# Next, write the offset to the new vtable in the
289296
# already-allocated SOffsetT at the beginning of this object:
@@ -308,20 +315,37 @@ def WriteVtable(self):
308315
encode.Write(
309316
packer.soffset,
310317
self.Bytes,
311-
self.Head(),
318+
self.head,
312319
SOffsetTFlags.py_type(vt2Offset - objectOffset),
313320
)
314321

315322
self.current_vtable = None
316323
return objectOffset
317324

325+
def WriteVtableEntries(self, entries):
326+
"""Write a contiguous block of VOffsetT values with a single prep call."""
327+
count = len(entries)
328+
if count == 0:
329+
return
330+
elem_size = N.VOffsetTFlags.bytewidth
331+
total_bytes = elem_size * count
332+
self.Prep(elem_size, total_bytes - elem_size)
333+
head = self.head - total_bytes
334+
self.head = UOffsetTFlags.py_type(head)
335+
pack = packer.voffset.pack_into
336+
buf = memoryview_type(self.Bytes)
337+
offset = head
338+
for value in entries:
339+
pack(buf, offset, value)
340+
offset += elem_size
341+
318342
def EndObject(self):
319343
"""EndObject writes data necessary to finish object construction."""
320344
self.assertNested()
321345
self.nested = False
322346
return self.WriteVtable()
323347

324-
def growByteBuffer(self):
348+
def GrowByteBuffer(self):
325349
"""Doubles the size of the byteslice, and copies the old data towards
326350
327351
the end of the new buffer (since we build the buffer backwards).
@@ -352,12 +376,15 @@ def Head(self):
352376
## @cond FLATBUFFERS_INTERNAL
353377
def Offset(self):
354378
"""Offset relative to the end of the buffer."""
355-
return UOffsetTFlags.py_type(len(self.Bytes) - self.Head())
379+
return UOffsetTFlags.py_type(len(self.Bytes) - self.head)
356380

357381
def Pad(self, n):
358382
"""Pad places zeros at the current offset."""
359-
for i in range_func(n):
360-
self.Place(0, N.Uint8Flags)
383+
if n <= 0:
384+
return
385+
new_head = self.head - n
386+
self.Bytes[new_head : self.head] = b"\x00" * n
387+
self.head = UOffsetTFlags.py_type(new_head)
361388

362389
def Prep(self, size, additionalBytes):
363390
"""Prep prepares to write an element of `size` after `additional_bytes`
@@ -374,15 +401,19 @@ def Prep(self, size, additionalBytes):
374401

375402
# Find the amount of alignment needed such that `size` is properly
376403
# aligned after `additionalBytes`:
377-
alignSize = (~(len(self.Bytes) - self.Head() + additionalBytes)) + 1
404+
head = self.head
405+
buf_len = len(self.Bytes)
406+
alignSize = (~(buf_len - head + additionalBytes)) + 1
378407
alignSize &= size - 1
379408

380409
# Reallocate the buffer if needed:
381-
while self.Head() < alignSize + size + additionalBytes:
382-
oldBufSize = len(self.Bytes)
383-
self.growByteBuffer()
384-
updated_head = self.head + len(self.Bytes) - oldBufSize
385-
self.head = UOffsetTFlags.py_type(updated_head)
410+
needed = alignSize + size + additionalBytes
411+
while head < needed:
412+
oldBufSize = buf_len
413+
self.GrowByteBuffer()
414+
buf_len = len(self.Bytes)
415+
head += buf_len - oldBufSize
416+
self.head = UOffsetTFlags.py_type(head)
386417
self.Pad(alignSize)
387418

388419
def PrependSOffsetTRelative(self, off):
@@ -457,7 +488,9 @@ def CreateSharedString(self, s, encoding="utf-8", errors="strict"):
457488
before calling CreateString.
458489
"""
459490

460-
if s in self.sharedStrings:
491+
if not self.sharedStrings:
492+
self.sharedStrings = {}
493+
elif s in self.sharedStrings:
461494
return self.sharedStrings[s]
462495

463496
off = self.CreateString(s, encoding, errors)
@@ -480,16 +513,17 @@ def CreateString(self, s, encoding="utf-8", errors="strict"):
480513
else:
481514
raise TypeError("non-string passed to CreateString")
482515

483-
self.Prep(N.UOffsetTFlags.bytewidth, (len(x) + 1) * N.Uint8Flags.bytewidth)
516+
payload_len = len(x)
517+
self.Prep(
518+
N.UOffsetTFlags.bytewidth, (payload_len + 1) * N.Uint8Flags.bytewidth
519+
)
484520
self.Place(0, N.Uint8Flags)
485521

486-
l = UOffsetTFlags.py_type(len(s))
487-
## @cond FLATBUFFERS_INTERNAL
488-
self.head = UOffsetTFlags.py_type(self.Head() - l)
489-
## @endcond
490-
self.Bytes[self.Head() : self.Head() + l] = x
522+
new_head = self.head - payload_len
523+
self.head = UOffsetTFlags.py_type(new_head)
524+
self.Bytes[new_head : new_head + payload_len] = x
491525

492-
self.vectorNumElems = len(x)
526+
self.vectorNumElems = payload_len
493527
return self.EndVector()
494528

495529
def CreateByteVector(self, x):
@@ -503,15 +537,13 @@ def CreateByteVector(self, x):
503537
if not isinstance(x, compat.binary_types):
504538
raise TypeError("non-byte vector passed to CreateByteVector")
505539

506-
self.Prep(N.UOffsetTFlags.bytewidth, len(x) * N.Uint8Flags.bytewidth)
540+
data_len = len(x)
541+
self.Prep(N.UOffsetTFlags.bytewidth, data_len * N.Uint8Flags.bytewidth)
542+
new_head = self.head - data_len
543+
self.head = UOffsetTFlags.py_type(new_head)
544+
self.Bytes[new_head : new_head + data_len] = x
507545

508-
l = UOffsetTFlags.py_type(len(x))
509-
## @cond FLATBUFFERS_INTERNAL
510-
self.head = UOffsetTFlags.py_type(self.Head() - l)
511-
## @endcond
512-
self.Bytes[self.Head() : self.Head() + l] = x
513-
514-
self.vectorNumElems = len(x)
546+
self.vectorNumElems = data_len
515547
return self.EndVector()
516548

517549
def CreateNumpyVector(self, x):
@@ -538,14 +570,14 @@ def CreateNumpyVector(self, x):
538570
else:
539571
x_lend = x.byteswap(inplace=False)
540572

541-
# Calculate total length
542-
l = UOffsetTFlags.py_type(x_lend.itemsize * x_lend.size)
543-
## @cond FLATBUFFERS_INTERNAL
544-
self.head = UOffsetTFlags.py_type(self.Head() - l)
545-
## @endcond
546-
547573
# tobytes ensures c_contiguous ordering
548-
self.Bytes[self.Head() : self.Head() + l] = x_lend.tobytes(order="C")
574+
payload = x_lend.tobytes(order="C")
575+
576+
# Calculate total length
577+
payload_len = len(payload)
578+
new_head = self.head - payload_len
579+
self.head = UOffsetTFlags.py_type(new_head)
580+
self.Bytes[new_head : new_head + payload_len] = payload
549581

550582
self.vectorNumElems = x.size
551583
return self.EndVector()
@@ -615,11 +647,11 @@ def __Finish(self, rootTable, sizePrefix, file_identifier=None):
615647

616648
self.PrependUOffsetTRelative(rootTable)
617649
if sizePrefix:
618-
size = len(self.Bytes) - self.Head()
650+
size = len(self.Bytes) - self.head
619651
N.enforce_number(size, N.Int32Flags)
620652
self.PrependInt32(size)
621653
self.finished = True
622-
return self.Head()
654+
return self.head
623655

624656
def Finish(self, rootTable, file_identifier=None):
625657
"""Finish finalizes a buffer, pointing to the given `rootTable`."""
@@ -634,8 +666,31 @@ def FinishSizePrefixed(self, rootTable, file_identifier=None):
634666

635667
## @cond FLATBUFFERS_INTERNAL
636668
def Prepend(self, flags, off):
637-
self.Prep(flags.bytewidth, 0)
638-
self.Place(off, flags)
669+
size = flags.bytewidth
670+
if size > self.minalign:
671+
self.minalign = size
672+
673+
head = self.head
674+
buf_len = len(self.Bytes)
675+
alignSize = (~(buf_len - head)) + 1
676+
alignSize &= size - 1
677+
678+
needed = alignSize + size
679+
while head < needed:
680+
oldBufSize = buf_len
681+
self.GrowByteBuffer()
682+
buf_len = len(self.Bytes)
683+
head += buf_len - oldBufSize
684+
685+
if alignSize:
686+
new_head = head - alignSize
687+
self.Bytes[new_head:head] = b"\x00" * alignSize
688+
head = new_head
689+
690+
N.enforce_number(off, flags)
691+
head -= size
692+
self.head = UOffsetTFlags.py_type(head)
693+
encode.Write(flags.packer_type, self.Bytes, head, off)
639694

640695
def PrependSlot(self, flags, o, x, d):
641696
if x is not None:
@@ -803,70 +858,47 @@ def ForceDefaults(self, forceDefaults):
803858
##############################################################
804859

805860
## @cond FLATBUFFERS_INTERNAL
806-
def PrependVOffsetT(self, x):
807-
self.Prepend(N.VOffsetTFlags, x)
808-
809861
def Place(self, x, flags):
810862
"""Place prepends a value specified by `flags` to the Builder,
811863
812864
without checking for available space.
813865
"""
814866

815867
N.enforce_number(x, flags)
816-
self.head = self.head - flags.bytewidth
817-
encode.Write(flags.packer_type, self.Bytes, self.Head(), x)
868+
new_head = self.head - flags.bytewidth
869+
self.head = UOffsetTFlags.py_type(new_head)
870+
encode.Write(flags.packer_type, self.Bytes, new_head, x)
818871

819872
def PlaceVOffsetT(self, x):
820873
"""PlaceVOffsetT prepends a VOffsetT to the Builder, without checking
821874
822875
for space.
823876
"""
824877
N.enforce_number(x, N.VOffsetTFlags)
825-
self.head = self.head - N.VOffsetTFlags.bytewidth
826-
encode.Write(packer.voffset, self.Bytes, self.Head(), x)
878+
new_head = self.head - N.VOffsetTFlags.bytewidth
879+
self.head = UOffsetTFlags.py_type(new_head)
880+
encode.Write(packer.voffset, self.Bytes, new_head, x)
827881

828882
def PlaceSOffsetT(self, x):
829883
"""PlaceSOffsetT prepends a SOffsetT to the Builder, without checking
830884
831885
for space.
832886
"""
833887
N.enforce_number(x, N.SOffsetTFlags)
834-
self.head = self.head - N.SOffsetTFlags.bytewidth
835-
encode.Write(packer.soffset, self.Bytes, self.Head(), x)
888+
new_head = self.head - N.SOffsetTFlags.bytewidth
889+
self.head = UOffsetTFlags.py_type(new_head)
890+
encode.Write(packer.soffset, self.Bytes, new_head, x)
836891

837892
def PlaceUOffsetT(self, x):
838893
"""PlaceUOffsetT prepends a UOffsetT to the Builder, without checking
839894
840895
for space.
841896
"""
842897
N.enforce_number(x, N.UOffsetTFlags)
843-
self.head = self.head - N.UOffsetTFlags.bytewidth
844-
encode.Write(packer.uoffset, self.Bytes, self.Head(), x)
898+
new_head = self.head - N.UOffsetTFlags.bytewidth
899+
self.head = UOffsetTFlags.py_type(new_head)
900+
encode.Write(packer.uoffset, self.Bytes, new_head, x)
845901

846902
## @endcond
847903

848-
849-
## @cond FLATBUFFERS_INTERNAL
850-
def vtableEqual(a, objectStart, b):
851-
"""vtableEqual compares an unwritten vtable to a written vtable."""
852-
853-
N.enforce_number(objectStart, N.UOffsetTFlags)
854-
855-
if len(a) * N.VOffsetTFlags.bytewidth != len(b):
856-
return False
857-
858-
for i, elem in enumerate(a):
859-
x = encode.Get(packer.voffset, b, i * N.VOffsetTFlags.bytewidth)
860-
861-
# Skip vtable entries that indicate a default value.
862-
if x == 0 and elem == 0:
863-
pass
864-
else:
865-
y = objectStart - elem
866-
if x != y:
867-
return False
868-
return True
869-
870-
871-
## @endcond
872904
## @}

0 commit comments

Comments
 (0)