Timing the time it takes to parse time
ayende.com
ayende.com
And if the numbers are so large, adjust the scale. Don't plot thousands of nanoseconds—plot microseconds!
I'm not faulting the author for their plots—making good plots is hard!—but good articles suffer when the data is presented in such ways.
I wonder what the speedup will be from omitting all the validation and associated branching and just picking the values out directly. Chances are that you won't see more than one date format in the same input either. Another strategy I'd use is to parallelise the digitisation; assuming the YYYY-MM-DDTHH:mm:ss format, it'd start off something like this (untested, but should be close if not completely correct):
Y = *(uint32_t*)s - 0x30303030;
M = *(uint16_t*)(s+5) - 0x3030;
D = *(uint16_t*)(s+8) - 0x3030;
Y = 100 * ((10 * (Y >> 24) + (Y >> 16)) & 255) +
((10 * (Y >> 8) + Y) & 255);
M = (10 * (M >> 8) + M) & 255;
D = (10 * (D >> 8) + D) & 255;
It should be possible to reduce the year computation to 2 multiplications, at the expense of several more masking operations. Around 50-100 clock cycles per full datetime should be possible, which is in the few-dozen-ns range on a GHz-clock CPU. Probably could go a bit faster still if you start bringing in the SSE... Y = *(uint32_t*)s - 0x30303030;
By casting a string to a uint32_t, you are assuming the memory layout of uint32_t. Y will have a different value depending on architecture. E.g. on a typical little-endian you will get 0x06010002. On big-endian you will get 0x02000106.I'm talking about C, however. I didn't notice the post is talking about C#. Things might be better defined there.
*(uint32_t*)s
will get you a SIGBUS if s is not aligned to a 4-byte boundary. Though that's another issue, not what OP was referring to.Anyway, the point of his post was about possible gains from removing validation, not about being portable or production code.
0x36313032 - 0x30303030 = 0x06010002
how is 0x02000106 the same as 0x06010002?
we want to turn the sequence [0x32, 0x30, 0x31, 0x36] (same on both architectures) into [0x00, 0x00, 0x07, 0xe0] in big endian or [0xe0, 0x07, 0x00, 0x00] in little endian. You can't simply perform the same procedure in both architectures since it'll result in a reversed sequence in one of them...
So it actually looks like it currently assumes a big-endian system.
Other parts that are always hot include split() and string concatenation. Java compilers can substitute StringBuffers when they see naive string concatenation, but in Python there's no easy way to build a string in a complex loop and you end up putting string fragments into a list and then finally join()ing them. Madness!
https://bugs.python.org/issue980695
PyPy and other implementations may do better with the join idiom though.
Of course someone with code spending a lot of time on joining strings can measure which is best for their situation, but += is fine for most things.
Its an old problem https://bitbucket.org/pypy/pypy/issues/1925/very-slow-string...
So whilst pypy is otherwise much faster than CPython, its missing of this kind of optimisation is why actually CPython can be faster for parsing my logs.
I know about this because I've been bitten by it :)
In [63]: timeit dateutil.parser.parse('2013-05-11T21:23:58.970460+07:00') 10000 loops, best of 3: 89.5 µs per loop
In [64]: timeit arrow.get('2013-05-11T21:23:58.970460+07:00') 10000 loops, best of 3: 62.1 µs per loop
In [65]: timeit numpy.datetime64('2013-05-11T21:23:58.970460+07:00') 1000000 loops, best of 3: 714 ns per loop
In [66]: timeit iso8601.parse_date('2013-05-11T21:23:58.970460+07:00') 10000 loops, best of 3: 23.9 µs per loop
> Other parts that are always hot include split() and string concatenation. Java compilers can substitute StringBuffers when they see naive string concatenation, but in Python there's no easy way to build a string in a complex loop and you end up putting string fragments into a list and then finally join()ing them. Madness!
The Python solution you describe is the same as in Java. If you have `String a = b + c + d;` then the compiler may optimize this using a StringBuffer as you say[1]. In Python it's also pretty cheap to do `a = b + c + d` to concatenate strings (or `''.join([b, c, d])`; but you should run a little microbenchmark to see which works best). But if it's in a "complex loop" as you opine then Java will certainly not do this. So you have to build a buffer using StringBuilder and then use toString() which is basically the same exact process except it has the name `builder.toString` instead of `''.join(builder)`
Unless of course you have some interesting insights into the jvm internals about string concatenation optimizations.
[1]http://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html...
Anyway, if you're installing packages from pip, may as well just install iso8601 and get the best performance - possibly beating .Net (who knows? as you said, I have a different machine than OP).
This is, as you can guess, somewhat fragile especially when some of the parts may be constants. So JEP 280 has changed javac to emit an invokeDynamic instruction with information about the constant and dynamic parts of the string so the optimisation strategy can be chosen at run time and can change over time without requiring everyone to recompile their java code.
All log lines began with a date+time like "2015-12-10 14:42:54.432" and there's maybe 100 lines per second. You can therefore just take the first 19 characters, parse that to a millisecond unix time and then separately parse the milliseconds to an int and add that. All you need is one cache entry (since logs are mostly in order) and then you can just do a string comparison (i.e. no hashmap lookup) to check the cache - instantly 100x fewer time parsing calls.
The best way to speed up a function is to not call it!
There are some compilers that can do it automatically with some hints, but I think they are mostly experimental not production.
Just adding memoization to date and time parsing gets you very little when there's little duplication of the inputs, and without the breaking apart of the data could very likely have yielded worse performance.
With my C module, I got 320ns, which was 62x faster than Python's less flexible strptime. See https://github.com/closeio/ciso8601/ for my benchmarks.
EDIT: I got downvoted, but it's just a question. If it's logs, is it old logs, or streaming from somewheere? Rather than speculate I'd like to hear the actual answer from thomas-st for their use case. 62x is just under 1.8 orders of magnitude, so I wouldn't think it matters in comparison with typical CPU and RAM speeds, disk, network, or other bottlenecks...
Depending on how much faster is "fast", these things can (dare I say, should) trump efficiency.
Essentially this is about optimisation vs premature optimisation. That's a horse dead for decades now.
Even allowing for other data e.g. a timestamp followed by some other data, at 19µs/datetime you can easily end up with that bottlenecking your entire pipeline if the datasource spews (which is common in contexts like HFT, aggregated logs and the like)
+1
This is why a little ELT goes a long way.
>Good CSV parsers reach 200MB/s
By good (and open source) we're talking about libcsv, rust-csv, and rust quick-csv[1]. If you're doing your own custom parsing you can write your own numeric parsers to remove support for parsing nan, inf, -inf, etc and drop scientific notation which will claw back a lot of the time. If you also know the exact width of the date field then you can also shave plenty of time parsing datetimes. But at that point, maybe write data to disk as protobuf or msgpack or avro, or whatever.
The 200MB/s, at least for rust-csv, is for "raw" parsing (handling CSV itself) not field parsing and conversions, so those would be additional costs.
> If you also know the exact width of the date field then you can also shave plenty of time parsing datetimes.
Yes if you can have fixed-size fields and remove things like escaping and quoting and the like things get much faster.
[1] - https://github.com/tstack/lnav/blob/master/src/ptimec.cc
I do lots of this in Ruby [0], and often write a custom parser when I know that dates or times will be of regular form. As a bonus I get to golf different methods of doing the parsing against each other, using microbenchmarks.
[0] https://gist.github.com/dougal/e972b26cd0293f99b41896e7d89c0...