PyPy v7.1 released; now uses UTF-8 internally for Unicode strings
morepypy.blogspot.com
morepypy.blogspot.com
From PyPy's Twitter feed:
"unicode-utf8 just got merged! Unicode strings are now internally represented as utf-8 in PyPy, with an optional extra index data structure to make indexing O(1). We'll write a blog post about it eventually."
Python strings are random access indexable. UTF-8 has variable length characters, so it is not directly randomly accessable. So the Python 3 representation inflates them to 1, 2 or 4 bytes per character, depending on the longest character in the string.
In practice, most strings are not random accessed. Most subscripts are +-1 from a previous subscript. So most actions can be optimized into uses of "advance one UTF-8 char" or "back up one UTF-8 char". Hard cases require generating an index array of the entire string. That takes a full string scan. Apparently that's what PyPy is doing.
This raises the cost of functions like "find()", which search the string for something and return an integer which is a position in the string suitable for random access use. I'd once suggested that "find", etc. return an opaque object which was really a byte index into the string. If you used that in a subscript, the right thing would happen. If you converted it to an integer, the index array would have to be computed. Is PyPy doing that, or does the use of "find" force index generation?
But they do expose the buffer interface which requires uniform size
* Is there ANY occurrence of the byte sequence within the 'string'?
(I only care if it's 'invalid', right at the start or end, etc)
* I'm about to use it to create a splice / as part of parsing.
I could be convinced to use strings.Split() if I knew the resulting objects were more like go's slices.
Thin array bounds wrappers indirectly pointing to the same backing array.Go could have returned a slice covering the rest of the string instead. Or maybe slices of "everything up to the match", "the matching part", and "everything after the match". That would avoid dealing with direct indices into a UTF-8 byte string. If the inputs are valid UTF-8, the outputs will be valid UTF-8. With indices, it's so easy to mess it up. Or worse, create a string parsing vunerability.
AÉBéC would give
0x41FF42FF42
where FF would mean, refer to another table with the index of the UTF8 char like the following:
---------------
|------|------|
|0 |1 |
|------|------|
|0xC389|0xC3A9|
|-------------|
This would make random access fast however will also increase overhead at other places.
Also IIRC, UTF8 doesn't use values 0x80 <= unused_utf8_values <= 0xFF. Value between 0x80 and 0xFF could be used to refer to an index of the table. eg 0x80 = index 0, 0x81 = index 1 ... 0xFE = index 126 and 0xFF = refer to other table.
Regardless of values over 0x80. The index in the special table will still have to be kept when iterating over it for find and stuff.
EDIT: table formatting
index => codepoint
1 => 0xc389
3 => 0xc3a9
This would require a binary search; we could change that to a hash table, to make it O(1) w/o too much trouble. However…This degrades pretty badly for strings that don't contain any/much ASCII (Greek, Russian, any Asian language, Indian, …); each of these table indexes is also presumably sizeof(size_t) (for the index) + 4 (for the codepoint), so 12B, probably 16 bytes/slot (after padding). That's 16B per codepoint. Perhaps some more could be squeezed out w/ some sort of variable length deal, but we're already in over our heads, I think.
> UTF8 doesn't use values 0x80 <= unused_utf8_values <= 0xFF.
UTF-8 uses all octet values up to an including 0xf5, except 0xc0 and 0xc1 (see the table[1]), so there's a few values, but not many.
Good idea, but it can lead to some hard to handle special cases. Consider for example running find() on a number of large strings and storing the opaque results in a list. Since the objects would require the strings to evaluate the opaque objects Python wouldn't be able to garbage collect them, potentially leading to hard-to-track-down and unexpected out-of-memory errors. One could mitigate the problem by using a buffer to limit the size of the strings kept alive in this way, but eh, it gets complicated. :)
I'd potentially willing to help with the development to get that working. Current day job is a 64-bit Windows shop in finance and we have several teams expanding their use of Python with very large datasets. A 64-bit PyPy on Windows could be big for us.
http://doc.pypy.org/en/release-2.6.x/windows.html#what-is-mi...
Please help us, we are a small team. The best way to reach us is through IRC on the #pypy channel
In the meantime, the python perf and performance packages are driving speed.python.org, and I started trying to get pypy3 runs reported there, but we need to provide a warmup parameter for each benchmark so that too will take some time.
The best benchmark of course is your use case, try it out and let us know where we are slow
The main speedup is in converting unicode to internal representation once, then passing around the object with no further conversions.
Not sure that answered the question though.