Added a new fast reading/writing engine to io.ascii#2716
Conversation
|
I look forward to looking this over in detail when I have the chance :) |
|
For the record here, there I looked at the timing dependence on field width and found some non-linear scaling in the fast branch. In particular if you have a table with floats that are 7 characters wide in total, then go to the full precision case where they are 18 characters wide:
http://nbviewer.ipython.org/gist/taldcroft/4ac6e43fad88810c6561 The other way to put it is that for the wide field case, the fast-c version is only 2.1 times faster than the pure python legacy version. For the narrow field case I reproduce your original benchmark that fast-c is about 6 times faster than pure python. The relative difference between fast-c-reader and pandas may point to some way to get some more speed. |
|
After looking over the conversion code in Pandas, I found it was using a custom string-to-double function called The value in the file is "6.2187331579177550499956283", and the fast reader before this latest commit had the same result as in |
|
Very interesting! Can you do some quick things to make the results a bit easier to interpret:
Definitely if this faster converter is not round tripping at the bit level then it can't be used. |
|
Hmmm, it does look like a bug in Pandas: |
|
Interesting. Have you filed a pandas issue? Maybe someone there will be able to make a quick fix. |
|
I've actually been managing a PR in Pandas which ignores empty/commented lines during reading, so I'll ask about the rationale behind this functionality (or if it's actually a bug)...it does seem like pretty weird behavior. |
|
Minor issue below. This gets me thinking that maybe we don't need the option of
The latter should be exactly the same in code as the current |
|
Ah--when I first added |
|
Update on the string to float conversion issue: it seems that After looking around, it seems there isn't a way to convert a string to a double quickly while staying within 0.5 ULP (e.g. The old way (using So it seems numpy's conversion is better for narrow precision, but my guess is this can vary quite a bit. Interestingly, I ran your IPython notebook on my machine (with
I wonder why |
|
@AmrAS1 - with regards to the Pandas |
|
@taldcroft - Hm, I'm not sure. So far I haven't seen a case in which |
|
Looks like |
|
@AmrAS1 - Very cool! I spent a little while playing with this because it seems pretty important to science-oriented folks that might actually care about ULPs. To understand things fully and make life simpler I ended up changing the random number to be 1.xxxxx instead of 0.xxxxx so that the ULP is basically always the same at around 2**-52. Otherwise you can randomly get strings of 0 at the beginning and end up with a smallish number. The way you did everything with Decimal is clever and makes everything work just fine even in the leading-0 case, it just took me a little longer to understand why. 😄 Also it seems like there was a possible uint8-rollover case that wasn't being handled. This only showed up when actually looking at the ULP values, but not in your final histogram. The way I got around this is a bit of a kludge. http://nbviewer.ipython.org/gist/taldcroft/c0034f876604673e62e8 Great plot, something like this should go in the docs (and maybe into your Pandas issue!). |
|
I'm not going to have time to look at this in any detail, but it looks fantastic, nice work @AmrAS1! One question: is there a reason to keep the "old" readers around at all if these work better for both reading and writing? That is, maybe there should be no "fast" and "slow" distinction at all, just replace the existing ones wholesale? (Or perhaps move them to the docs as examples but not be in the code itself anymore) |
|
@eteq - Thanks! My guess is that there are probably a couple corner cases I haven't yet discovered in which the old and new readers perform differently, since there are so many cases to deal with in the C tokenizer. I think it would be okay in most cases to replace the old readers with the new ones, but maybe some file out there can be read only with I guess this is related to the issue @taldcroft brought up previously, which is whether |
|
Ok, that makes sense. I agree that it makes sense for |
|
@AmrAS1 - In the current scheme, the default for |
|
Yep, good idea -- I just pushed a commit adding |
|
@eteq and anyone else listening, on our weekly tagup we decided eliminate the
Same applies for Since the default is true, most of the time you would see this keyword in code would be like: In this case the keyword name is spot-on and accurate. |
|
The multiprocessing branch is currently passing on Travis and seems pretty stable, so I just merged it into this PR. Once I change the behavior of |
|
Great, thanks for the update, @taldcroft. Sounds good to me. |
|
This latest commit should fix the speed issue @taldcroft noted with http://nbviewer.ipython.org/gist/amras1/91b3a4129517009b2765 The fix is a bit of a hack even though it works fine for now, so at some point it'll be worth looking into #2823 for a more general solution. |
|
Now that the multiprocessing branch has been merged and I fixed up a couple speed issues (particularly in string conversion and tokenization), I think the analysis using the IPython notebook @taldcroft wrote for reading is basically final. I modified it slightly and ran it again with these results: http://nbviewer.ipython.org/github/amras1/ascii-profiling/blob/master/ascii_read_bench.ipynb The notebook now uses a temporary file instead of Apparently IPython doesn't work well with multiprocessing, so I couldn't get reliable results by adding |
|
Fastest reader in the West! This is just awesome @AmrAS1. |
|
@taldcroft - Thanks! Looking over one of Wes McKinney's blog posts, it seems the rationale behind storing strings as Python objects is the memory advantage. This brings up the issue of memory efficiency, which unfortunately the fast reader doesn't seem so hot on; a sample run of the heap profiler Massif on a 10,000 line float file indicated that the new reader takes up far more memory than Pandas, and in fact about 25% more than the legacy reader. I'm pretty sure this has to do with the way the C tokenizer stores output data, so the only way to change this is to restructure the tokenizer. Since the main priority at this point is to write documentation and make sure everything works correctly, I'll table this (no pun intended 😄) for now and hopefully find a solution once everything is completed (or in a separate PR). |
|
@astrofrog - In fact the fast readers / writers are the new default, but there is a fallback to the original readers if any exception occurs. So we do need to be careful about this, but the test fixtures have been updated so that essentially all of the (relevant) existing tests are now being run with both fast and slow readers. For a change of this complexity we're really relying on the tests, so that's probably the area to focus any final review. Before merging I'll give that another scan and look for any holes. |
There was a problem hiding this comment.
Although in this case there's no reason other than convention, please use super(FastBasic, self).__init__ instead of FastBasic.__init__. Likewise in the following classes.
There was a problem hiding this comment.
I agree, might as well fix these now.
|
@taldcroft - ok, sounds good! But since as you said there is a fallback to the original readers, this should be safe (except against segfaults of course). But in any case, the sooner we merge, the more testing will happen before 1.0 :) I'm too short on time to review the full code here but as long as @embray and/or @mdboom have had a chance to look at this, then I'm fine with it. |
There was a problem hiding this comment.
My one concern about this class (and I haven't gone through it in detail) is that it seems like there are many areas where it doesn't take full advantage of being written in Cython as opposed to normal Python (other than being able to use the tokenizer easily without having to define a full Python interface to the tokenizer.
THAT SAID, even if that's the case, I don't think it should hold up merging. I'm just pointing out that there are probably some rich opportunities to further optimization in here in a few places :)
|
@embray - I opened a couple of issues to capture your two open comments. |
|
This latest commit fixes the issue @embray noted with calls to |
|
One minor item: It's not obvious to me what the defaults are for Also, perhaps we should add a "What's new" section about this? It certainly shouldn't hold up this PR if it's going to take any significant amount of time, but at some point it's certainly worth "headlining" this as a major improvement that will come in 1.0. And it's fresh in @AmrAS1's mind now, so it might be a good time to write it. (Unless @embray or @astrofrog think we want to only do what's new stuff right before release?) Other than that, I'm happy with this and am 👍 to merge! |
|
Looks like that's mentioned once in the docs, but I can add a notice in docstrings somewhere as well. As for the "what's new" addition, should I just begin a new file with an empty "overview" section and just write a section on fast/reading writing for now? |
|
I would propose holding off on the What's New section until a little closer to the next release. That will give this update a chance to get used and to sink in, and it's possible there will be tweaks in the meantime. Also this will be one of the bigger highlights so we'll want to take a little time to make sure the text is good and gets properly reviewed. It looks like everyone is agreeing that this is ready to merge, which I would like to do tomorrow afternoon if nobody else has any concerns. |
71a5b92 to
b3407fa
Compare
b3407fa to
81e0742
Compare
|
@AmrAS1 - I'm merging now! I will fix the doc string mistake in master. |
Added a new fast reading/writing engine to io.ascii
|
Oops, must've been a copy-paste error. Thanks for the catch! |
|
Thank you @AmrAS1 for the great contribution! |
|
@astrofrog - you're welcome, glad to have this merged at last! |
|
Typo fixed in c027ce8. |
|
@AmrAS1 @taldcroft - sure, delaying What's new is fine too - just want to make sure it's in everyone's head as a feature we'll want to highlight there! And the change @AmrAS1 made in |
This PR adds a Cython/C parsing engine to the infrastructure of
io.asciiso that new reading classes for simple formats (currentlyFastBasic,FastTab,FastCsv,FastNoHeader,FastCommentedHeader, andFastRdb) can opt to use this engine instead of the ordinaryBaseReaderinheritance model. The ordinary readers for these formats still remain to maintain flexibility and compatibility. The PR introduces a new parameteruse_fast_readerinascii.read()which can be used to enable or disable the fast engine (by default,read()will attempt to use the fast reader and fall back on the slow reader if parsing fails). The fast readers are also on the guess list directly in front of their slower counterparts.There is also a Cython engine for writing with these formats; a similar parameter
use_fast_writercan enable or disable this engine, andascii.write()uses the engine by default when writing output in a compatible format.Using randomly generated text files, it seems that the fast reading engine is anywhere from 3 to 7 times faster than the ordinary readers depending on the data type of table columns, while the fast writing engine is anywhere from ~2.5 to 12 times faster than the ordinary writers; both engines have less of a speed gain for floating-point data, which might be worth looking into at some point. Here are some before and after benchmarks, which cover a range of ASCII formats and compare Astropy's performance with that of numpy and Pandas. There is also a short design document here, which gives a quick overview of the parsing implementation in this PR.
Please feel free to make any suggestions or ask me anything.