FastDoubleParser: Java port of Daniel Lemires fast_double_parser
github.com
github.com
ch = ++index < strlen ? str.charAt(index) : 0;
If I saw that code scattered across in some java code, I would immediately be like - "rewrite please".But the same code fragment in C would be like - yeah, we need a bounds check, good job, can you make it a macro please.
The java default Float parser was somewhat of a complete perf headache when multi-threading, more than about single core consumption (till jdk8b96 - JDK-7032154), so in the past Hive has shipped with its own strod impl[1] to avoid the central lock in parseDouble.
Also it worked off a raw utf8 byte[] stream, which is more common coming out of a disk file or network serialization rather than a CharSequence.
[1] - https://github.com/apache/hive/blob/4446414f4478091db1eb20bc...
Code should be written for humans.
They could produce something that looks like Qt, OWL/VCL, or nowadays .NET like but in C++.
Instead they produce libraries where one is forced to use C low level details, mixed with Win32 and COM pointer handling and casts everywhere, not even MFC escapes it.
Not to count how many interactions of COM smart pointers that have come up with since COM exists.
C++/CX looked like they finally learned their way and would offer something similar to C++ Builder, but no that wasn't meant for us to enjoy it.
So now anyone doing UWP with C++, gets to manually edit IDL files without tooling support, and merge the generated files manually.
I really don't get their macho developer culture.
In retrospect, do you feel it was it a bad idea to not use a heavier abstraction?
It was a bad idea versus what other compiler vendors had in their toolbox.
MFC only won because Microsoft is the OS vendor, and Borland lost it trying to fly too high.
Back in the MS-DOS and Windows 3.x very few would use Microsoft compilers, Borland and Watcom were the favourite.
I'm inclined to not recognize that as a distinct culture and rather call it an infectious habit. Auditable, maintainable code is a skill, after all.
let ch = if index < strlen then str.charAt(index) else 0
Eminently readable and less boring than long if/else chains in Python.On a side note, having gotten used to newer languages, an option type sure is a nice thing.
>>> foo = 10
>>> bar = foo if foo > 5 else 0
>>> bar
10
>>> bar = foo if foo < 5 else 0
>>> bar
0"for ch in index:"
reads pretty cleanly to me.
I'd far prefer:
++index;
ch = index < strlen ? str.charAt(index) : 0;
I don't see a good reason to make it a macro, though. index += 1;
if (index < strlen) {
ch = str.charAt(index);
} else {
ch = 0;
}
If I was using kotlin, I'd do it even cleaner: inline fun String.at(index: Int): Char? =
if (index >= 0 && index < length) charAt(index)
else null
[...]
index += 1
ch = str.at(index) ?: 0;
The bytecode is exactly the same, but I’d argue it’s much cleanerC++:
bool found_minus = (*p == '-');
bool negative = false;
if (found_minus) {
++p;
negative = true;
if (!is_integer(*p)) { // a negative sign must be followed by an integer
return nullptr;
}
}
Java: int strlen = str.length();
if (strlen == 0) {
throw new NumberFormatException("empty String");
}
int index = 0;
char ch = str.charAt(index);
boolean negative = false;
if (ch == '-') {
negative = true;
ch = ++index < strlen ? str.charAt(index) : 0;
if (ch == 0) {
throw new NumberFormatException("'-' cannot stand alone");
}
}I have seen far too many bugs creep in this way. I feel like C/C++ induce a desire to keep the code as short as possible and being 'clever'; even I often feel the need to do that in C++ when I am happy to write a more readable version in other programming languages.
The bugs are also hard to spot when skimming through the code: When there's a lot of code you will often miss the ++ on the 'index' as it's a long line compared to having '++index' on its own line.
Another good 'pet peeve' of mine is the following construct:
for(int i = 10; i--;)
// Do something until 'i' reaches 0
So many off-by-one errors...The source of charAt is also interesting because it manually bounds checks while the array access into the underlying char[] has its own bounds check. I'm not sure why having the separate exception is better than letting the array access throw directly?
https://github.com/AdoptOpenJDK/openjdk-jdk11/blob/19fb8f93c...
>> I'm not sure why having the separate exception is better than letting the array access throw directly?
(1) The original String charAt method (Sun/Oracle) had to account for an offset and an index. The exception from the bounds check on the underlying char[] would be misleading/confusing... and therefore the birth of the seperate StringIndexOutOfBoundsException
Source: http://www.docjar.com/html/api/java/lang/String.java.html
(2) Because of the above and for backward compatibility, the OpenJDK is doing the same exception handling... sometimes it pays to review the original source code to get some context and rationale to the OpenJDK version.
Swift originally had an option named -Ofast that just turned off all the safety features - eventually it got renamed to -Ounchecked.
I still don't think it's very descriptive though. Whether or not it's unsafe depends a lot on the domain. In most of my code, I don't care about the sign of zero or floating point associativity and want my compiler to auto convert division to multiplication by reciprocals. Better would be something like -fnon-compliant-float-math or something.
Although I guess -fno-trapping-math is a bit different from the other optimizations which could actually be "unsafe" in a different way, since signaling nans are quite useful for debugging.
Apple M1 OpenJDK 64-Bit Server VM, Azul Systems, Inc., 16+36
parsing random integers in the range [0,1)
=== number of trials 32 =====
FastDoubleParser.parseDouble MB/s avg: 492.324205, min: 479.57, max: 516.26
Double.parseDouble MB/s avg: 111.509085, min: 110.63, max: 114.65
Speedup FastDoubleParser vs Double: 4.415104
Apple M1 OpenJDK 64-Bit Server VM, Azul Systems, Inc., 16+36
parsing integers in file data/canada.txt
read 111126 lines
=== number of trials 32 =====
FastDoubleParser.parseDouble MB/s avg: 483.642065, min: 420.43, max: 502.15
Double.parseDouble MB/s avg: 104.347746, min: 99.03, max: 116.91
Speedup FastDoubleParser vs Double: 4.634907
https://github.com/ulfjack/ryu/blob/master/src/main/java/inf...
Aside, my JMathTeX fork uses Ryū to achieve real-time rendering of TeX in Java.
(Though 1/100M randomly tested conversions had a 14ULP inaccuracy, so I guess the inaccuracy is a bit unpredictable).
Anyway, it's all great programming but it seems to me a symptom or a larger social failure, where we are making bad protocols faster instead of making the protocols better.
That's why I'm building Concise Encoding [1], which is a twin text/binary format that can be 1:1 converted back and forth. You do everything in binary, and only convert to/from text in the few places where a human needs to be involved (like debugging or configuration).
The type field is almost always 1 byte long, except for typed arrays (which would generally in aggregate be long enough to offset the cost) and two uncommon types/variations. These have an effective 2-byte type field (0x94 selects a secondary type plane, and the next byte selects the specific type from there).
The most common integer values (-100 to 100) are encoded directly into the types 0x9c to 0x64 (wraparound), such that interpreting them directly as signed 8-bit signed integers yields their actual value (type 0x00 = integer 0, type 0x64 = integer 100, type 0x9c = -100, etc).
Strings are also optimized such that types 0x80 - 0x8f are used for the most common string lengths (0 to 15) so as not to require a separate length field.
The rest have a 1-byte type field. There are also 3 reserved type codes left in the first plane in case something big comes up in the future. You can see the chart here: https://github.com/kstenerud/concise-encoding/blob/master/cb...
Lemire's really has made a huge contribution to computer science
I also don’t think parsing doubles will ever be the bottleneck in any of my Java code. If it turns out it is, though...
In my C# and even in C++, profiler sometimes showed me I am bottlenecked on parsing or printing floats. Happened quite a few times.
In all cases the data was 3D models or similar things. STL, 3MF, VTK, G-mesh, G-code are all text based, either always (looking at you, 3MF consortium) or as an option (STL and some versions of VTK can be either binary or text). Even low-end modern GPUs can easily render 1E+7 triangles these days, thus high-poly 3D models are not uncommon.
Games are not affected because they preprocess the hell out of their assets, optimizing everything they can about the data format. But in CAD/CAM stuff you’re pretty much guaranteed to bottleneck on parsing numbers when loading these text-based formats.
Though I would have added a unit test to iterate over complete 2^64 set of values and compare the output to Double.parseDouble().
Also, while there are only 2^64 distinct double values, there are many more string representations than that. For example, "1" and "1.0" are the same underlying value, but a parser needs to be able to handle both.
unit testing doesn't mean that all possible values have to be tested. usually it's about code paths, i.e. standard, error and edge cases.
In this particular case, I would have tested 2^N where N>32 random, unique values, at a minimum.
Of course, it depends on who the customer is. Some customers can tolerate a few bugs here and there; and some would incur million dollar losses.