So, a couple of days ago I read a blogpost on the PyPy Status Blog written by Maciej Fijalkowski about speeding up JSON encoding in PyPy. Fijal is one of the core developers of PyPy, and in his blogpost he explains how he edited the JSON encoder to better fit the PyPy style (remember that many python files in PyPy are still tuned for running on cPython, and can be improved with simple changes).
I don’t have any particular suggestions different from the ones presented by Fijal, just want to point out that JITViewer is really useful for understanding where the code could be improved, and that sometimes just being smart can save quite a bit of computation time.
I spent some time trying to improve the string concatenation of decoder.py. The current version builds a list of chunks that are eventually joined together to build the concatenated string. This appears to be actually the fastest way (in this instance, at least): I tried the __pypy__.builders.StringBuilder and the classic += concatenation, but they turn out to be less efficient (so, mission failed, here).
I instead changed the way JSONObject builds its list of key-value elements: from Python 2.7 the decoder supports a new parameter, object_pairs_hook, for preserving ordering and multiple instances of keys. For supporting it, JSONObject builds a list of tuples storing each key-value parsed, converting it in a dictionary if no object_pairs_hook is given; the point here is that building a list of tuples is much slower than building a dictionary, and we would probably prefer to avoid this if we actually don’t need a list of pairs. This simple improvements helped speeding up the decoder a bit, achieving some good results.
I tested the new code on PyPy 1.8.1-dev0 using two benchmarks: a modification of pypy/benchmarks/json_bench (which currently evaluates only encoding, and not decoding) and kracekumar/cerealization, here the results:
| PyPy 1.8.1-dev0 | PyPy 1.8.1-dev0 MOD | Speedup | |
|---|---|---|---|
| Run #1 | 37.3687298298 | 32.1885719299 | +16.09% |
| Run #2 | 37.5971119404 | 31.1700959206 | +20.62% |
| Run #3 | 37.4318089485 | 31.1591031551 | +20.13% |
| Run #4 | 37.3458600044 | 31.0966379642 | +20.10% |
| Run #5 | 37.3631129265 | 31.117978096 | +20.07% |
| Run #6 | 37.3080999851 | 31.0478279591 | +20.16% |
| Run #7 | 37.2737519741 | 31.0621140003 | +20.00% |
| Run #8 | 37.4270458221 | 30.9801039696 | +20.81% |
| Run #9 | 37.3104338646 | 31.004087925 | +20.34% |
| Average | 37.3806616995 | 31.2029467689 | +19.80% |
| Variance | 0.0093633203 | 0,1407598879 | |
| Results on pypy/benchmarks/json_bench | |||
| cPython 2.6.1 | cPython 2.7.2 | PyPy 1.8.1-dev0 | PyPy 1.8.1-dev0 MOD | |
|---|---|---|---|---|
| Dump: json | 26.6461949348 | 9.7420539856 | 2.9267168045 | 2.90510392189 |
| Dump: simplejson | 13.1301739216 | 12.8199129105 | 13.8134858608 | 13.8025200367 |
| Dump: cPickle | 10.0216720104 | 9.97437906265 | 25.7030470371 | 26.0591058731 |
| Dump: tnetstrings | 31.7227909565 | 30.9078440666 | 2.97724795341 | 2.96262979507 |
| Load: json | 99.1293950081 | 13.0056598186 | 10.1231429577 | 7.314661026 (+38.39%) |
| Load: simplejson | 6.15336108208 | 5.84075093269 | 10.8849618435 | 10.9000110626 |
| Load: cPickle | 4.93965697289 | 4.70230102539 | 12.822371006 | 13.0902838707 |
| Load: tnetstrings | 38.5776751041 | 43.1720809937 | 6.17516303062 | 6.17008113861 |
| Results on kracekumar/celeralization | ||||
Code is available on BitBucket.