-
Notifications
You must be signed in to change notification settings - Fork 5.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize String.toFloatOrNull() #5364
base: master
Are you sure you want to change the base?
Conversation
The existing implementation used a regular expression which caused memory allocations, which are expensive on mobile devices. In addition, a custom parser can outperform regular expressions. The new implementation is compatible with the original regular expression and performs ~22x faster on OpenJDK 22 with a 2021 MacBook Pro M1 Pro: Benchmark Mode Cnt Score Error Units KotlinBenchmark.customParser thrpt 482.020 ops/ms KotlinBenchmark.regex thrpt 21.471 ops/ms On a Pixel 6 running Android 14, the new implementation is ~225x faster: 8,595,686 ns 10428 allocs Trace ColorBenchmark.isFloatRegex 37,755 ns 0 allocs Trace ColorBenchmark.isFloat It also has the benefit of never allocating anything (vs ~940 allocations per invocation for the existing implementation).
libraries/stdlib/jvm/src/kotlin/text/StringNumberConversionsJVM.kt
Outdated
Show resolved
Hide resolved
Co-authored-by: Jake Wharton <[email protected]>
I just realized I could parse 4 digits at a time in the non-hexadecimal case. I'll give it a try to see if it helps performance. |
@romainguy could you please also provide a link to benchmark sources? |
Here is the JVM benchmark: https://gist.github.com/romainguy/7acca58a1401ba9361bd93deb778e11e And with the recent changes the custom parser is slightly faster as well:
|
Parsing four digits at a time (applied only to the fractional part where it's more likely to be useful) only helps if the data set contains a lot of strings where there are more than 4 digits after the period. It would help when parsing SVG files for instance, but it's not a clear win for the general case, so probably not worth adding considering the added implementation complexity. It does hurt the case when the fraction part has < 4 digits (on the dataset linked above, performance goes from 23x faster down to 20x faster on JVM). For reference, here's a function that returns 4 when it can successfully parse 4 digits from the input string at a given offset, otherwise it returns 0 (so the call site can do private inline fun parseFourDigits(str: String, offset: Int): Int {
val v = (str[offset + 0].code.toLong() or
(str[offset + 1].code.toLong() shl 16) or
(str[offset + 2].code.toLong() shl 32) or
(str[offset + 3].code.toLong() shl 48))
val base = v - 0x0030003000300030L
val predicate = v + 0x0046004600460046L or base
return if (predicate and 0xff80_ff80_ff80_ff80UL.toLong() == 0L) 4 else 0
} |
The existing implementation used a regular expression which caused memory allocations, which are expensive on mobile devices. In addition, a custom parser can outperform regular expressions.
The new implementation is compatible with the original regular expression and performs ~22x faster on OpenJDK 22 with a 2021 MacBook Pro M1 Pro:
On a Pixel 6 running Android 14, the new implementation is ~225x faster:
It also has the benefit of never allocating anything (vs ~940 allocations per invocation for the existing implementation).