Skip to content

Commit

Permalink
feat: more performance optimizations (#92)
Browse files Browse the repository at this point in the history
The two big changes here are bypassing __init__ in a special case and
directly setting instance attributes via reaching into self.__dict__.
These are optimizations in a potential hot path that were isolated via
exhaustive profiling and experimentation and save ~%16 in certain benchmarks.

Do not try this at home.
  • Loading branch information
software-dov authored Aug 5, 2020
1 parent 6094f9f commit 19b1519
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 17 deletions.
32 changes: 15 additions & 17 deletions proto/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ def __new__(mcls, name, bases, attrs):
#
# m = MyMessage()
# MyMessage.field in m
mcls = type("AttrsMeta", (mcls,), opt_attrs)
if opt_attrs:
mcls = type("AttrsMeta", (mcls,), opt_attrs)

# Determine the filename.
# We determine an appropriate proto filename based on the
Expand Down Expand Up @@ -295,7 +296,10 @@ def wrap(cls, pb):
pb: A protocol buffer object, such as would be returned by
:meth:`pb`.
"""
return cls(pb, __wrap_original=True)
# Optimized fast path.
instance = cls.__new__(cls)
instance.__dict__["_pb"] = pb
return instance

def serialize(cls, instance) -> bytes:
"""Return the serialized proto.
Expand All @@ -319,11 +323,7 @@ def deserialize(cls, payload: bytes) -> "Message":
~.Message: An instance of the message class against which this
method was called.
"""
# Usually we don't wrap the original proto and are force to make a copy
# to prevent modifying user data.
# In this case it's perfectly reasonable to wrap the proto becasue it's
# never user visible, and it gives a huge performance boost.
return cls(cls.pb().FromString(payload), __wrap_original=True)
return cls.wrap(cls.pb().FromString(payload))

def to_json(cls, instance) -> str:
"""Given a message instance, serialize it to json
Expand Down Expand Up @@ -373,7 +373,7 @@ def __init__(self, mapping=None, **kwargs):
if mapping is None:
if not kwargs:
# Special fast path for empty construction.
self._pb = self._meta.pb()
self.__dict__["_pb"] = self._meta.pb()
return

mapping = kwargs
Expand All @@ -383,15 +383,13 @@ def __init__(self, mapping=None, **kwargs):
# that it will not have side effects on the arguments being
# passed in.
#
# The `__wrap_original` argument is private API to override
# this behavior, because `MessageRule` actually does want to
# wrap the original argument it was given. The `wrap` method
# on the metaclass is the public API for this behavior.
if not kwargs.pop("__wrap_original", False):
mapping = copy.copy(mapping)
self._pb = mapping
# The `wrap` method on the metaclass is the public API for taking
# ownership of the passed in protobuf objet.
mapping = copy.copy(mapping)
if kwargs:
self._pb.MergeFrom(self._meta.pb(**kwargs))
mapping.MergeFrom(self._meta.pb(**kwargs))

self.__dict__["_pb"] = mapping
return
elif isinstance(mapping, type(self)):
# Just use the above logic on mapping's underlying pb.
Expand Down Expand Up @@ -420,7 +418,7 @@ def __init__(self, mapping=None, **kwargs):
params[key] = pb_value

# Create the internal protocol buffer.
self._pb = self._meta.pb(**params)
self.__dict__["_pb"] = self._meta.pb(**params)

def __bool__(self):
"""Return True if any field is truthy, False otherwise."""
Expand Down
12 changes: 12 additions & 0 deletions tests/test_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,15 @@ class Squid(proto.Message):
s = Squid()
with pytest.raises(AttributeError):
getattr(s, "shell")


def test_setattr():
class Squid(proto.Message):
mass_kg = proto.Field(proto.INT32, number=1)

s1 = Squid()
s2 = Squid(mass_kg=20)

s1._pb = s2._pb

assert s1.mass_kg == 20

0 comments on commit 19b1519

Please sign in to comment.