Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 14 additions & 16 deletions Lib/json/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,17 +264,6 @@ def floatstr(o, allow_nan=self.allow_nan,

def _make_iterencode(markers, _default, _encoder, _indent, _floatstr,
_key_separator, _item_separator, _sort_keys, _skipkeys, _one_shot,
## HACK: hand-optimized bytecode; turn globals into locals
ValueError=ValueError,
dict=dict,
float=float,
id=id,
int=int,
isinstance=isinstance,
list=list,
str=str,
tuple=tuple,
_intstr=int.__repr__,
):

def _iterencode_list(lst, _current_indent_level):
Expand Down Expand Up @@ -311,7 +300,7 @@ def _iterencode_list(lst, _current_indent_level):
# Subclasses of int/float may override __repr__, but we still
# want to encode them as integers/floats in JSON. One example
# within the standard library is IntEnum.
yield buf + _intstr(value)
yield buf + int.__repr__(value)
elif isinstance(value, float):
# see comment above for int
yield buf + _floatstr(value)
Expand Down Expand Up @@ -374,7 +363,7 @@ def _iterencode_dict(dct, _current_indent_level):
key = 'null'
elif isinstance(key, int):
# see comment for int/float in _make_iterencode
key = _intstr(key)
key = int.__repr__(key)
elif _skipkeys:
continue
else:
Expand All @@ -399,7 +388,7 @@ def _iterencode_dict(dct, _current_indent_level):
yield 'false'
elif isinstance(value, int):
# see comment for int/float in _make_iterencode
yield _intstr(value)
yield int.__repr__(value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this affect performance for a large list of integers?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like doing things the "normal" way (int.__repr__) is about 0-10% faster on a little microbenchmark:

LOOPS = 1 << 24

def outer():
    _intstr = int.__repr__
    def old():
        for _ in range(LOOPS):
            _intstr(42)
    def new():
        for _ in range(LOOPS):
            int.__repr__(42)
    return old, new

def bench(func):
    start = time.perf_counter()
    func()
    end = time.perf_counter()
    print(end - start)

old, new = outer()
bench(old)
bench(new)

I think the combination of cell + bound method unwrapping is enough overhead to cancel out the gains from avoiding a (cached) global lookup and a (cached) attribute load. But it's pretty much in the noise anyways, so probably not worth keeping the old micro-optimization.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bench shows only 1-2% difference on my machine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. On my machine, this bench shows 2-3% difference on 3.14 and main and 20-25% difference on 3.12 and 3.13. So this micro-optimization was justified in the past, but perhaps not so much now. It cannot be backported if you are planning to backport this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No plans to backport.

elif isinstance(value, float):
# see comment for int/float in _make_iterencode
yield _floatstr(value)
Expand Down Expand Up @@ -434,7 +423,7 @@ def _iterencode(o, _current_indent_level):
yield 'false'
elif isinstance(o, int):
# see comment for int/float in _make_iterencode
yield _intstr(o)
yield int.__repr__(o)
elif isinstance(o, float):
# see comment for int/float in _make_iterencode
yield _floatstr(o)
Expand All @@ -458,4 +447,13 @@ def _iterencode(o, _current_indent_level):
raise
if markers is not None:
del markers[markerid]
return _iterencode

def _iterencode_once(o, _current_indent_level):
nonlocal _iterencode, _iterencode_dict, _iterencode_list
try:
yield from _iterencode(o, _current_indent_level)
finally:
# Break reference cycles due to mutually recursive closures:
del _iterencode, _iterencode_dict, _iterencode_list

return _iterencode_once
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Break reference cycles created by each call to :func:`json.dump` or
:meth:`json.JSONEncoder.iterencode`.
Loading