Since I was mentioned by name in part 3, perhaps I can provide some interesting commentary:
> All this code had recently been rewritten pretty much from scratch by Luke Shumaker ... While this code is very clean and generic, with a good separation of the multiple levels of abstractions, such as bytes and codepoints, that would make it very easy to extend the escaping logic, it isn’t taking advantage of many assumptions convert_UTF8_to_JSON could make to take shortcuts.
My rewritten version was already slightly faster than the original version, so I didn't feel the need to spend more time optimizing it, at least until the simple version got merged; which I had no idea when that'd be because of silence from the then-maintainer. Every optimization would be an opportunity for more pain when rebasing away merge-conflicts; which was already painful enough the 2 times I had to do it while waiting for a reply.
> One of these for instance is that there’s no point validating the UTF-8 encoding because Ruby did it for us and it’s impossible to end up inside convert_UTF8_to_JSON with invalid UTF-8.
I don't care to dig through the history to see exactly what changed when, but: At the time I wrote it, the unit tests told me that wasn't true; if I omitted the checks for invalid UTF-8, then the tests failed.
> Another is that there are only two multi-byte characters we care about, and both start with the same 0xE2 byte, so the decoding into codepoints is a bit superfluous. ... we can re-use Mame’s lookup table, but with a twist.
I noted in the original PR description that I thought a lookup table would be faster than my decoder. I didn't use a lookup table myself (1) to keep the initial version simple to make code-review simple to increase likelihood that it got merged, and (2) the old proprietary CVTUTF code used a lookup table, and because I was so familiar with the CVTUTF code, I didn't feel comfortable being the one to to re-add a lookup table. Glad to see that my suspicion was correct and that someone else did the work!
I'm not familiar with the internals of the JSON gem, but in general... yeah, it's funny right? PRs are almost never ideal. Always some compromise based on time available, code review considerations, etc.
Everything you said makes a lot of sense!
Yes, it's something I changed before merging your patch.
I didn't mean to say your patch wasn't good or anything It was very much appreciated.
Which modes are that? https://github.com/ohler55/oj/blob/develop/pages/Modes.md#oj...
I tried:
Oj.dump(obj, mode: :strict)
and a few others and none seemed faster than `json 2.9.1` on the benchmarks I use.Edit:
Also most of these mode simply aren't correct in my opinion:
>> Oj.dump(999.9999999999999, { mode: :compat })
=> "999.9999999999999"
>> Oj.dump(999.9999999999999, { mode: :strict })
=> "1000"The callback parsers (Saj and Scp) also show a performance advantage as does the most recent Oj::Parser.
As for the dumping of floats that are at the edge of precision (16 places), Oj does round to to 15 places if the last 4 of a 16 digit float is "0001" or "9999" if the float precision is not set to zero. That is intentional. If that is not the desired behavior and the Ruby conversion is preferred then setting the float precision to zero will not round. You picked the wrong options for your example.
I would like to say that the core json has a come a very long way since Oj was created and is now outstanding. If the JSON gem had started out where it is now I doubt I would have bothered writing Oj.
The spec doesn't specify a precision or range limit anywhere (just suggests that IEEE754 might be a reasonable target for interoperability, but that supports up to 64bit floats, and it looks like Oj is dropping to 32bit floats?).
Python and Go don't go and change the precision of floating point numbers in their implementations, but according to the standard, they're entirely entitled to, and so is Oj.
I don't see anything in https://github.com/ohler55/oj/blob/develop/pages/Modes.md#oj... specifying that Strict will force floating points to specific precision vs other implementations
I've spent the last years in Python land, recently heavily LLM assisted, but I'm itching to do something with Ruby (and or Rails) again.
The annyoing thing about it is that all the workarounds I know about are really ain't that pretty:
1. You can hard-code the check against it and return a hardcoded string representation of it:
if (number == -9223372036854775808) return "-9223372036854775808";
By the way, "(number && (number == -number))" condition doesn't work so don't try to be too smart about it: just compare against INT_MIN/LONG_MIN/etc.2. You can widen the numeric type, and do the conversion in the larger integer width, but it doesn't really work for intmax_t and it's, of course, is slower. Alternatively, you can perform only the first iteration in the widened arithmetic, and do the rest in the original width, but this leads to some code duplication.
2a. You can do
unsigned unumber = number;
if (number < 0) unumber = -unumber;
and convert the unsigned number instead. Again, you can chose to do only the first iteration in the unsigned, on platforms where unsigned multiplication/division is slower than signed ones. Oh, and again, beware that "unsigned unumber = number < 0 ? -number : number" way of conversion doesn't work.3. You can, instead of turning the negative numbers into positive ones and working with the positive numbers, do the opposite: turn positive numbers into negative ones and work exclusively with the negative numbers. Such conversion is safe, and division in C is required to truncate to zero, so it all works out fine except for the fact that the remainders will be negative; you'll have to deal with that.
But yeah, converting integers into strings is surprisingly slow; not as slow as converting floats, but still very noticeable. Maybe BCDs weren't such a silly idea, after all...
Does anyone know why Intel publish a DFP (decimal floating point) library instead of pushing those instructions down to the microcode level like the mainframes do ?
We've had a few months of pretty regular Ruby posts now, and the last week has had one almost every single day.
I'm not a regular Rubyist, but I'm glad to see the language getting more attention.
The lore I was familiar with was that a stood for ascii.
> in this listing of man pages from Third Edition Unix (1973) collected by Dennis Ritchie himself, it does contain the line:
> > atoi(III): convert ASCII to integer
> In fact, even the first edition Unix (ca 1971) man pages list atoi as meaning Ascii to Integer.
It's also in the FreeBSD [1], NetBSD [2] and OpenBSD [3] atoi man pages.
[1]: https://man.freebsd.org/cgi/man.cgi?query=atoi&sektion=3
As for the int-to-string function, using the division result to do a faster modulus (eg with the div function) and possibly a lookup table seem like they’d help (there must be some good open source libraries focused on this to look at).
It depends, presumably the generated JSON string would quickly be written down inside something else (e.g. sent as HTTP response or saved in database), so the object slot would be freed rather quickly.
Format strings are compilable in principle, so that:
snprintf(buf, sizeof buf, "%ld", long_value);
can just turn into some compiler-specific run-time function. The compiler also can tell when the buffer is obviously large enough to hold any possible value, and use a function that doesn't need the size.How common is that, though?
Common Lisp's format function can accept a function instead of a format string. The arguments are passed to that function and it is assumed to do the job:
(format t (lambda (...) ...) args ...)
There is a macro called formatter which takes a format string, and compiles it to such a function. [8]> (format t "~1,05f" pi)
3.14159
NIL
[9]> (format t "~10,5f" pi)
3.14159
NIL
[10]> (format t (formatter "~10,5f") pi)
3.14159
NIL
[11]> (macroexpand '(formatter "~10,5F"))
#'(LAMBDA (STREAM #:ARG3345 &REST #:ARGS3342) (DECLARE (IGNORABLE STREAM))
(LET ((SYSTEM::*FORMAT-CS* NIL)) (DECLARE (IGNORABLE #:ARG3345 #:ARGS3342))
(SYSTEM::DO-FORMAT-FIXED-FLOAT STREAM NIL NIL 10 5 NIL NIL NIL #:ARG3345) #:ARGS3342)) ;
T
In this implementation, formatter takes "~10,5f" and spins it into a (system::do-format-fixed-float ...) call where the field width and precision arguments are constants. Just the stream and numeric argument are passed in, along with a bunch of other arguments that are defaulted to nil.I think CL implementations are allowed to apply formatter implicitly, which would make sense at least in code compiled for speed.
Just think: this stuff existed before there was a GNU C compiler. It was a huge progress when it started diagnosing mismatches between format strings literal and printf arguments.
Maybe at the end, he should have shown the two profiles again for comparison :D