Repository: aio-libs/aiohttp
Issue category: Performance Issue

Issue:
Payload registry has to do a linear search to find payloads
https://github.com/aio-libs/aiohttp/blob/21f5f92a755dc5ac7225b5e76f561553cf86565e/aiohttp/payload.py#L97

There is a note that its inefficent.

We might be able to clean it up by giving each payload a key as part of the base class and then doing a dict lookup instead


Hints:
> We might be able to clean it up by giving each payload a key as part of the base class and then doing a dict lookup instead

Doesn't really work, because it works with isinstance checks, which could be a subclass or an abc. I thought about this previously and didn't think of any improvements. Not sure what zope.interface is though, might be worth looking into.
Maybe if we have a property that is a key thats the same for the subclassed object.. not sure if that will work.  Needs some more thought.


Not sure what you mean, the subclasses are e.g. subclass of bytes or (more realistically) a subclass of io.BufferedIO. They are not our classes, just arbitrary types that might be used to store/stream data.
I need to dig at it a bit more to come up with a solution. This one will take a bit of thought so I opened the issue so I don't forget to come back to it.
Looking at this
We can likely store the payload type() and then do a dict lookup on the type() of the incoming and we get no matches because it's subclasses, fallback to the linear search 
We don't have a benchmark for simple post requests which makes this a bit more difficult to quantify what the performance drag of the linear search is.  We should add a benchmark first 

Patch:
diff --git a/aiohttp/payload.py b/aiohttp/payload.py
index ea50b6a38cb..9979ed269b6 100644
--- a/aiohttp/payload.py
+++ b/aiohttp/payload.py
@@ -101,6 +101,7 @@ def __init__(self) -> None:
         self._first: List[_PayloadRegistryItem] = []
         self._normal: List[_PayloadRegistryItem] = []
         self._last: List[_PayloadRegistryItem] = []
+        self._normal_lookup: Dict[Any, PayloadType] = {}
 
     def get(
         self,
@@ -109,12 +110,20 @@ def get(
         _CHAIN: "Type[chain[_PayloadRegistryItem]]" = chain,
         **kwargs: Any,
     ) -> "Payload":
+        if self._first:
+            for factory, type_ in self._first:
+                if isinstance(data, type_):
+                    return factory(data, *args, **kwargs)
+        # Try the fast lookup first
+        if lookup_factory := self._normal_lookup.get(type(data)):
+            return lookup_factory(data, *args, **kwargs)
+        # Bail early if its already a Payload
         if isinstance(data, Payload):
             return data
-        for factory, type in _CHAIN(self._first, self._normal, self._last):
-            if isinstance(data, type):
+        # Fallback to the slower linear search
+        for factory, type_ in _CHAIN(self._normal, self._last):
+            if isinstance(data, type_):
                 return factory(data, *args, **kwargs)
-
         raise LookupError()
 
     def register(
@@ -124,6 +133,11 @@ def register(
             self._first.append((factory, type))
         elif order is Order.normal:
             self._normal.append((factory, type))
+            if isinstance(type, Iterable):
+                for t in type:
+                    self._normal_lookup[t] = factory
+            else:
+                self._normal_lookup[type] = factory
         elif order is Order.try_last:
             self._last.append((factory, type))
         else:
@@ -159,7 +173,8 @@ def __init__(
             self._headers[hdrs.CONTENT_TYPE] = content_type
         else:
             self._headers[hdrs.CONTENT_TYPE] = self._default_content_type
-        self._headers.update(headers or {})
+        if headers:
+            self._headers.update(headers)
 
     @property
     def size(self) -> Optional[int]:
@@ -228,9 +243,6 @@ class BytesPayload(Payload):
     def __init__(
         self, value: Union[bytes, bytearray, memoryview], *args: Any, **kwargs: Any
     ) -> None:
-        if not isinstance(value, (bytes, bytearray, memoryview)):
-            raise TypeError(f"value argument must be byte-ish, not {type(value)!r}")
-
         if "content_type" not in kwargs:
             kwargs["content_type"] = "application/octet-stream"
 
@@ -238,8 +250,10 @@ def __init__(
 
         if isinstance(value, memoryview):
             self._size = value.nbytes
-        else:
+        elif isinstance(value, (bytes, bytearray)):
             self._size = len(value)
+        else:
+            raise TypeError(f"value argument must be byte-ish, not {type(value)!r}")
 
         if self._size > TOO_LARGE_BYTES_BODY:
             warnings.warn(


Test patch: