Repository navigation
Optimize PyUnicode_FromString for short ASCII strings #81529
Description
Activity
_PyUnicode_FromASCII(s, len) is faster than PyUnicode_FromString(s) because PyUnicode_FromString() uses temporary _PyUnicodeWriter to support UTF-8.
But _PyUnicode_FromASCII("hello", strlen("hello"))
is not as easy asPyUnicode_FromString("hello")`._PyUnicode_FROM_ASCII() is simple macro which wraps _PyUnicode_FromASCII which calls strlen() automatically:
/* Convenient wrapper for _PyUnicode_FromASCII */ #define _PyUnicode_FROM_ASCII(s) _PyUnicode_FromASCII((s), strlen(s))I believe recent compilers optimize away calls of strlen().
- added3.9 (EOL)end of lifeend of lifeinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usagePerformance or resource usage
on Jun 20, 2019 #define _PyUnicode_FROM_ASCII(s) _PyUnicode_FromASCII((s), strlen(s))
LGTM.
I confirmed at least one measurable speedup:
./python -m pyperf timeit -s 'd={}' -- 'repr(d)'
FromString: Mean +- std dev: 157 ns +- 3 ns
FROM_ASCII: Mean +- std dev: 132 ns +- 3 ns>>> (157-132)/157 0.1592356687898089
Should we make these APIs public?
- rename
_PyUnicode_FromASCIItoPyUnicode_FromASCIIAndSize. - add
PyUnicode_FromASCIIas static inline function (without ABI). - add
#define _PyUnicode_FromASCII PyUnicode_FromASCIIAndSizefor source code compatibility.
- rename
Most of changes are in not performance sensitive code. I do not think there is a benefit of using new macro there.
If PyUnicode_FromString() is slow I prefer to optimize it instead of adding yet one esoteric private function for internal needs.
In case of dict.__repr__() we can get even more gain by using _Py_IDENTIFIER or more general API proposed by Victor.
Most of changes are in not performance sensitive code. I do not think there is a benefit of using new macro there.
Because I used just sed.
If PyUnicode_FromString() is slow I prefer to optimize it instead of adding yet one esoteric private function for internal needs.
OK. There are some code like
PyUnicode_FromString(name). Optimizing PyUnicode_FromString will be more effective. I created PR 14273.In case of dict.__repr__() we can get even more gain by using _Py_IDENTIFIER or more general API proposed by Victor.
Of course, I used it just for micro benchmarking. Optimizing it is not a goal. In case of PR 14273:
$ ./python -m pyperf timeit -s 'd={}' -- 'repr(d)' ..................... Mean +- std dev: 138 ns +- 2 ns
- changed the title
[-]add _PyUnicode_FROM_ASCII macro[/-][+]Optimize PyUnicode_GetString for short ASCII strings[/+]on Jun 20, 2019 Oh, wait. Why we used _PyUnicodeWriter here?
Decoding UTF-8 must not require it. 2-pass is enough._PyUnicode_FromASCII(s, len) is faster than PyUnicode_FromString(s) because PyUnicode_FromString() uses temporary _PyUnicodeWriter to support UTF-8.
I don't understand how _PyUnicodeWriter could be slow. It does not overallocate by default. It's just wrapper to implement efficient memory management.
Oh, wait. Why we used _PyUnicodeWriter here?
To optimize decoding errors: the error handler can use replacement string longer than 1 character. Overallocation is used in this case.
I don't understand how _PyUnicodeWriter could be slow. It does not overallocate by default. It's just wrapper to implement efficient memory management.
I misunderstood _PyUnicodeWriter. I thought it caused one more allocation, but it doesn't.
But _PyUnicodeWriter is still slow, because gcc and clang are not smart enough to optimize _PyUnicodeWriter_Init() & _PyUnicodeWriter_Prepare().
See this example:
#define PY_SSIZE_T_CLEAN #include <Python.h> #define S(s) (s),strlen(s) int main(int argc, char *argv[]) { Py_Initialize(); for (int i=0; i<100000000; i++) { //PyObject *s = PyUnicode_FromString("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); PyObject *s = _PyUnicode_FromASCII(S("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); Py_DECREF(s); } return 0; }PyUnicode_FromString() takes about 4 sec on my machine. _PyUnicode_FromASCII() is about 2 sec.
By skipping _PyUnicodeWriter for ASCII string (GH-14283), PyUnicode_FromString() takes about 3 sec.$ time ./x # PyUnicode_FromString real 0m4.085s user 0m4.081s sys 0m0.004s $ time ./y # PyUnicode_FromString (skip _PyUnicode_Writer, python/cpython#58491) real 0m2.988s user 0m2.988s sys 0m0.000s $ time ./z # _PyUnicode_FromASCII $ time ./z real 0m1.975s user 0m1.975s sys 0m0.000sAnother micro benchmark:
$ ./python-master -m pyperf timeit -o m1.json 'b=b"foobar"' -- 'b.decode()' ..................... Mean +- std dev: 93.1 ns +- 2.4 ns $ ./python -m pyperf timeit -o m2.json 'b=b"foobar"' -- 'b.decode()' ..................... Mean +- std dev: 83.1 ns +- 2.6 ns $ ./python -m pyperf compare_to m1.json m2.json Mean +- std dev: [m1] 93.1 ns +- 2.4 ns -> [m2] 83.1 ns +- 2.6 ns: 1.12x faster (-11%)I'm confused by the issue title: PyUnicode_GetString() doesn't exist, it's PyUnicode_FromString() :-) I changed the title.
- changed the title
[-]Optimize PyUnicode_GetString for short ASCII strings[/-][+]Optimize PyUnicode_FromString for short ASCII strings[/+]on Jun 21, 2019 Could you please measure the performance for long strings (1000, 10000 and 100000 characters): a long ASCII string and a long ASCII string ending with a non-ASCII character?
This optimization is only for short strings. There is no significant difference for long and non-ASCII strings.
# 1000 ASCII $ ./python -m pyperf timeit --compare-to=./python-master -s 'b=b"f"*1000' -- 'b.decode()' python-master: ..................... 196 ns +- 1 ns python: ..................... 185 ns +- 1 ns Mean +- std dev: [python-master] 196 ns +- 1 ns -> [python] 185 ns +- 1 ns: 1.06x faster (-6%) # 10000 ASCII $ ./python -m pyperf timeit --compare-to=./python-master -s 'b=b"f"*10000' -- 'b.decode()' python-master: ..................... 671 ns +- 4 ns python: ..................... 662 ns +- 1 ns Mean +- std dev: [python-master] 671 ns +- 4 ns -> [python] 662 ns +- 1 ns: 1.01x faster (-1%) # 100000 ASCII $ ./python -m pyperf timeit --compare-to=./python-master -s 'b=b"f"*100000' -- 'b.decode()' python-master: ..................... 5.86 us +- 0.03 us python: ..................... 5.85 us +- 0.02 us Mean +- std dev: [python-master] 5.86 us +- 0.03 us -> [python] 5.85 us +- 0.02 us: 1.00x faster (-0%) # 1 non-ASCII $ ./python -m pyperf timeit --compare-to=./python-master -s 'b="あ".encode()' -- 'b.decode()' python-master: ..................... 138 ns +- 2 ns python: ..................... 136 ns +- 2 ns Mean +- std dev: [python-master] 138 ns +- 2 ns -> [python] 136 ns +- 2 ns: 1.02x faster (-2%) # 1000 ASCII + 1 non-ASCII $ ./python -m pyperf timeit --compare-to=./python-master -s 'b=b"x"*1000 + "あ".encode()' -- 'b.decode()' python-master: ..................... 361 ns +- 9 ns python: ..................... 360 ns +- 5 ns Mean +- std dev: [python-master] 361 ns +- 9 ns -> [python] 360 ns +- 5 ns: 1.00x faster (-0%) Not significant! # 10000 ASCII + 1 non-ASCII $ ./python -m pyperf timeit --compare-to=./python-master -s 'b=b"x"*10000 + "あ".encode()' -- 'b.decode()' python-master: ..................... 2.83 us +- 0.02 us python: ..................... 2.83 us +- 0.03 us Mean +- std dev: [python-master] 2.83 us +- 0.02 us -> [python] 2.83 us +- 0.03 us: 1.00x slower (+0%) Not significant!
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields: