Skip to content

Commit b00e5b7

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 5998472 commit b00e5b7

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

@@ -255,6 +254,7 @@ def WriteVtable(self):
255254
i = len(self.current_vtable) - 1
256255
trailing = 0
257256
trim = True
257+
vt_entries = []
258258
while i >= 0:
259259
off = 0
260260
elem = self.current_vtable[i]
@@ -270,18 +270,25 @@ def WriteVtable(self):
270270
off = objectOffset - elem
271271
trim = False
272272

273-
self.PrependVOffsetT(off)
273+
vt_entries.append(VOffsetTFlags.py_type(off))
274274

275275
# The two metadata fields are written last.
276276

277277
# First, store the object bytesize:
278278
objectSize = UOffsetTFlags.py_type(objectOffset - self.objectEnd)
279-
self.PrependVOffsetT(VOffsetTFlags.py_type(objectSize))
279+
vt_entries.append(VOffsetTFlags.py_type(objectSize))
280280

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

286293
# Next, write the offset to the new vtable in the
287294
# already-allocated SOffsetT at the beginning of this object:
@@ -306,20 +313,37 @@ def WriteVtable(self):
306313
encode.Write(
307314
packer.soffset,
308315
self.Bytes,
309-
self.Head(),
316+
self.head,
310317
SOffsetTFlags.py_type(vt2Offset - objectOffset),
311318
)
312319

313320
self.current_vtable = None
314321
return objectOffset
315322

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

322-
def growByteBuffer(self):
346+
def GrowByteBuffer(self):
323347
"""Doubles the size of the byteslice, and copies the old data towards
324348
325349
the end of the new buffer (since we build the buffer backwards).
@@ -350,12 +374,15 @@ def Head(self):
350374
## @cond FLATBUFFERS_INTERNAL
351375
def Offset(self):
352376
"""Offset relative to the end of the buffer."""
353-
return UOffsetTFlags.py_type(len(self.Bytes) - self.Head())
377+
return UOffsetTFlags.py_type(len(self.Bytes) - self.head)
354378

355379
def Pad(self, n):
356380
"""Pad places zeros at the current offset."""
357-
for i in range_func(n):
358-
self.Place(0, N.Uint8Flags)
381+
if n <= 0:
382+
return
383+
new_head = self.head - n
384+
self.Bytes[new_head : self.head] = b"\x00" * n
385+
self.head = UOffsetTFlags.py_type(new_head)
359386

360387
def Prep(self, size, additionalBytes):
361388
"""Prep prepares to write an element of `size` after `additional_bytes`
@@ -372,15 +399,19 @@ def Prep(self, size, additionalBytes):
372399

373400
# Find the amount of alignment needed such that `size` is properly
374401
# aligned after `additionalBytes`:
375-
alignSize = (~(len(self.Bytes) - self.Head() + additionalBytes)) + 1
402+
head = self.head
403+
buf_len = len(self.Bytes)
404+
alignSize = (~(buf_len - head + additionalBytes)) + 1
376405
alignSize &= size - 1
377406

378407
# Reallocate the buffer if needed:
379-
while self.Head() < alignSize + size + additionalBytes:
380-
oldBufSize = len(self.Bytes)
381-
self.growByteBuffer()
382-
updated_head = self.head + len(self.Bytes) - oldBufSize
383-
self.head = UOffsetTFlags.py_type(updated_head)
408+
needed = alignSize + size + additionalBytes
409+
while head < needed:
410+
oldBufSize = buf_len
411+
self.GrowByteBuffer()
412+
buf_len = len(self.Bytes)
413+
head += buf_len - oldBufSize
414+
self.head = UOffsetTFlags.py_type(head)
384415
self.Pad(alignSize)
385416

386417
def PrependSOffsetTRelative(self, off):
@@ -455,7 +486,9 @@ def CreateSharedString(self, s, encoding="utf-8", errors="strict"):
455486
before calling CreateString.
456487
"""
457488

458-
if s in self.sharedStrings:
489+
if not self.sharedStrings:
490+
self.sharedStrings = {}
491+
elif s in self.sharedStrings:
459492
return self.sharedStrings[s]
460493

461494
off = self.CreateString(s, encoding, errors)
@@ -478,16 +511,17 @@ def CreateString(self, s, encoding="utf-8", errors="strict"):
478511
else:
479512
raise TypeError("non-string passed to CreateString")
480513

481-
self.Prep(N.UOffsetTFlags.bytewidth, (len(x) + 1) * N.Uint8Flags.bytewidth)
514+
payload_len = len(x)
515+
self.Prep(
516+
N.UOffsetTFlags.bytewidth, (payload_len + 1) * N.Uint8Flags.bytewidth
517+
)
482518
self.Place(0, N.Uint8Flags)
483519

484-
l = UOffsetTFlags.py_type(len(s))
485-
## @cond FLATBUFFERS_INTERNAL
486-
self.head = UOffsetTFlags.py_type(self.Head() - l)
487-
## @endcond
488-
self.Bytes[self.Head() : self.Head() + l] = x
520+
new_head = self.head - payload_len
521+
self.head = UOffsetTFlags.py_type(new_head)
522+
self.Bytes[new_head : new_head + payload_len] = x
489523

490-
self.vectorNumElems = len(x)
524+
self.vectorNumElems = payload_len
491525
return self.EndVector()
492526

493527
def CreateByteVector(self, x):
@@ -501,15 +535,13 @@ def CreateByteVector(self, x):
501535
if not isinstance(x, compat.binary_types):
502536
raise TypeError("non-byte vector passed to CreateByteVector")
503537

504-
self.Prep(N.UOffsetTFlags.bytewidth, len(x) * N.Uint8Flags.bytewidth)
538+
data_len = len(x)
539+
self.Prep(N.UOffsetTFlags.bytewidth, data_len * N.Uint8Flags.bytewidth)
540+
new_head = self.head - data_len
541+
self.head = UOffsetTFlags.py_type(new_head)
542+
self.Bytes[new_head : new_head + data_len] = x
505543

506-
l = UOffsetTFlags.py_type(len(x))
507-
## @cond FLATBUFFERS_INTERNAL
508-
self.head = UOffsetTFlags.py_type(self.Head() - l)
509-
## @endcond
510-
self.Bytes[self.Head() : self.Head() + l] = x
511-
512-
self.vectorNumElems = len(x)
544+
self.vectorNumElems = data_len
513545
return self.EndVector()
514546

515547
def CreateNumpyVector(self, x):
@@ -536,14 +568,14 @@ def CreateNumpyVector(self, x):
536568
else:
537569
x_lend = x.byteswap(inplace=False)
538570

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

548580
self.vectorNumElems = x.size
549581
return self.EndVector()
@@ -613,11 +645,11 @@ def __Finish(self, rootTable, sizePrefix, file_identifier=None):
613645

614646
self.PrependUOffsetTRelative(rootTable)
615647
if sizePrefix:
616-
size = len(self.Bytes) - self.Head()
648+
size = len(self.Bytes) - self.head
617649
N.enforce_number(size, N.Int32Flags)
618650
self.PrependInt32(size)
619651
self.finished = True
620-
return self.Head()
652+
return self.head
621653

622654
def Finish(self, rootTable, file_identifier=None):
623655
"""Finish finalizes a buffer, pointing to the given `rootTable`."""
@@ -632,8 +664,31 @@ def FinishSizePrefixed(self, rootTable, file_identifier=None):
632664

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

638693
def PrependSlot(self, flags, o, x, d):
639694
if x is not None:
@@ -801,70 +856,47 @@ def ForceDefaults(self, forceDefaults):
801856
##############################################################
802857

803858
## @cond FLATBUFFERS_INTERNAL
804-
def PrependVOffsetT(self, x):
805-
self.Prepend(N.VOffsetTFlags, x)
806-
807859
def Place(self, x, flags):
808860
"""Place prepends a value specified by `flags` to the Builder,
809861
810862
without checking for available space.
811863
"""
812864

813865
N.enforce_number(x, flags)
814-
self.head = self.head - flags.bytewidth
815-
encode.Write(flags.packer_type, self.Bytes, self.Head(), x)
866+
new_head = self.head - flags.bytewidth
867+
self.head = UOffsetTFlags.py_type(new_head)
868+
encode.Write(flags.packer_type, self.Bytes, new_head, x)
816869

817870
def PlaceVOffsetT(self, x):
818871
"""PlaceVOffsetT prepends a VOffsetT to the Builder, without checking
819872
820873
for space.
821874
"""
822875
N.enforce_number(x, N.VOffsetTFlags)
823-
self.head = self.head - N.VOffsetTFlags.bytewidth
824-
encode.Write(packer.voffset, self.Bytes, self.Head(), x)
876+
new_head = self.head - N.VOffsetTFlags.bytewidth
877+
self.head = UOffsetTFlags.py_type(new_head)
878+
encode.Write(packer.voffset, self.Bytes, new_head, x)
825879

826880
def PlaceSOffsetT(self, x):
827881
"""PlaceSOffsetT prepends a SOffsetT to the Builder, without checking
828882
829883
for space.
830884
"""
831885
N.enforce_number(x, N.SOffsetTFlags)
832-
self.head = self.head - N.SOffsetTFlags.bytewidth
833-
encode.Write(packer.soffset, self.Bytes, self.Head(), x)
886+
new_head = self.head - N.SOffsetTFlags.bytewidth
887+
self.head = UOffsetTFlags.py_type(new_head)
888+
encode.Write(packer.soffset, self.Bytes, new_head, x)
834889

835890
def PlaceUOffsetT(self, x):
836891
"""PlaceUOffsetT prepends a UOffsetT to the Builder, without checking
837892
838893
for space.
839894
"""
840895
N.enforce_number(x, N.UOffsetTFlags)
841-
self.head = self.head - N.UOffsetTFlags.bytewidth
842-
encode.Write(packer.uoffset, self.Bytes, self.Head(), x)
896+
new_head = self.head - N.UOffsetTFlags.bytewidth
897+
self.head = UOffsetTFlags.py_type(new_head)
898+
encode.Write(packer.uoffset, self.Bytes, new_head, x)
843899

844900
## @endcond
845901

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

0 commit comments

Comments
 (0)