Skip to content

Few optimizations for FITS tables - #4985

Merged
pllim merged 7 commits into
astropy:masterfrom
saimn:fits-table-optims
Sep 7, 2016
Merged

pllim merged 7 commits into
astropy:masterfrom
saimn:fits-table-optims

Conversation

@saimn

@saimn saimn commented May 27, 2016

Copy link
Copy Markdown
Contributor

I have FITS files with a lot of extensions, lot of tables with many columns (a few thousands sometimes ...) and it takes a significant time to load everything, so I tried to find some optimizations (btw snakeviz is very nice to look at profiling results !).
The changes here could be seen a micro-optimizations, but they concern methods which are called many times, so it makes a difference in the end.

  • Reference: Astropy 1.2.dev15480, Time to load my file : 2.8s
  • Change on _is_int (ncalls=1621317). Time after : 2.64 s
  • Change on Coldefs.__getattr__ (ncalls=2673). Time after : 1.73s
  • Change on ColumnAttribute.__get__ (ncalls=1626425). Time after : 1.6s

Maybe @embray if you have some time ?

Comment thread astropy/io/fits/column.py
# lists of column/field definition common names and keyword names, make
# sure to preserve the one-to-one correspondence when updating the list(s).
# Use lists, instead of dictionaries so the names can be displayed in a
# preferred order.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpicking: change list to tuples in the comment, too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed.

@pllim pllim added the io.fits label Jun 1, 2016
Comment thread astropy/io/fits/util.py Outdated
@@ -42,6 +42,8 @@ class StringIO(object):
elif six.PY2:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, but I just notice that this logic here is not compatible with Python 4.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the PY3 test.

@saimn

saimn commented Jun 2, 2016

Copy link
Copy Markdown
Contributor Author

@pllim @MSeifert04 - With 3af359c I now cache the lists of attributes in ColDefs instead of calling too many times __getattr__ (for name, format, ...). This results in a 2.5x speedup (just for this commit) ! I hope I handle correctly all the cases when the cache must be updated, at least the tests pass.

@embray - Maybe, if you want have a look ?

Master:

~/dev/astropy master*
❯ python -m timeit -n 5 -r 5 -s "from astropy.table import Table" "Table.read('table.fits')"
5 loops, best of 5: 2.44 sec per loop

This branch:

~/dev/astropy fits-table-optims*
❯ python -m timeit -n 5 -r 5 -s "from astropy.table import Table" "Table.read('table.fits')"
5 loops, best of 5: 485 msec per loop

Comment thread astropy/io/fits/column.py Outdated
"""Get or update local lists of column attributes"""
attrs = [name] if name is not None else KEYWORD_ATTRIBUTES
for name in attrs:
setattr(self, name + 's', self.__getattr__(name+'s'))

@pllim pllim Jun 2, 2016 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised PEP8 test didn't complain about lack of white space around +. Since it didn't complain, I guess we can leave this as is.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not switched on for E225

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, though there is no real obligation for spaces around operators ;-)

If operators with different priorities are used, consider adding whitespace around the operators with the lowest priority(ies). Use your own judgment; however, never use more than one space, ...

http://pep8.org/#other-recommendations

@pllim

pllim commented Jun 2, 2016

Copy link
Copy Markdown
Member

Overall, the speed-up looks nice. Like you said, the tests passed. So unless someone takes this for a spin on a variety of FITS tables, it is good enough?

@astrofrog astrofrog added this to the v1.3.0 milestone Jun 2, 2016
@saimn
saimn force-pushed the fits-table-optims branch from 3bdecf5 to 8fc380a Compare June 7, 2016 07:29
@saimn

saimn commented Jun 7, 2016

Copy link
Copy Markdown
Contributor Author

Rebased to fix conflict with master.

Comment thread astropy/io/fits/column.py
attr.append(val)
else:
attr.append('')
attr.append(val if val is not None else '')

@astrofrog astrofrog Aug 10, 2016 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@saimn - I think (val or '') would work too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@astrofrog - I think so, but the original code uses is not None. I'm not sure if there are case where val could be a falsy value that should not be translated to a empty string. So, in doubt, better stick with the original implementation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, what @saimn said.

This is a micro-optimization but _is_int can be called many many times when
loading FITS tables, so it can start to be meaningful.
@saimn
saimn force-pushed the fits-table-optims branch from 8fc380a to a3e8f7c Compare September 5, 2016 15:16
Comment thread astropy/io/fits/column.py
if attr == 'name':
del self.names
elif attr == 'format':
del self.formats

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't quite work--these also need to be invalidated when columns are added/removed. Otherwise I'm fine with the general concept. My preference would still be to not make any changes in the ColDefs class and instead update the FITS_rec class to be less reliant on the ColDefs object in general (an object I would like to get rid of, ultimately).

That said, in the interest of not wasting any more of your time on this and getting it finished, if you can just fix the column add/remove bug and ensure all the tests are passing this is definitely good enough for now! 👍

I also noticed you cleaned up a little bit of additional code rot--thanks for that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Erik ! The column addition/removal is now fixed.

When you say FITS_rec class to be less reliant on the ColDefs, I wonder for example why ``FITS_rec.namestries first to use_coldefs.names` and then `dtype.names`, and not the opposite ?

Based on our discussion here, I tried to make a few changes in fitsrec.py. This is on this gist, should I include this commit ?

  • replace self._coldefs.names with self.names
  • move indexing to self._coldefs when possible
  • and change the way to get names using dtype.names first

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you say FITS_rec class to be less reliant on the ColDefs, I wonder for example why `FITS_rec.names tries first to use _coldefs.names and then dtype.names, and not the opposite ?

Exactly--I think the opposite makes more sense. The way it works currently may be a remnant of when the dtype fields and the "logical" columns represented by the ColDefs class were not guaranteed to be the same. The reason for this is the unfortunate existence in FITS of "zero width" columns which must be supported--it's rare but does occur in the wild. It used to be that the Numpy dtype could not accommodate that, so there were a lot of hacks to make all the columns (including zero-width ones) represented to the user, while only representing the non-zero columns in the actual dtype.

I've gotten a bunch of patches into Numpy, however, to allow fields with zero size to work in a consistent manner, so most of those hacks have been removed (there is still one remnant of this issue: numpy/numpy#6430 Though that reminds me, the fix for that will be in Numpy 1.12 so in principle it should be possible to avoid use of the realign_dtype hack for Numpy 1.12 and up).

The patch in the gist looks good to me. I haven't tested it, and can't say with certainty that there are some dark corners lurking where that would actually break. But I don't think there--it's definitely what I had in mind.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok great ! I pushed the commit for fitsrec.

@MSeifert04

Copy link
Copy Markdown
Contributor

@saimn Since this is quite a small PR could you squash the commits into one and include a changelog entry (just to mention the improved runtime)? Maybe wait until the tests have finished to see if anything went wrong.

I'm not sure this is strictly necessary here so maybe wait if someone else wants to second that.

I'm really interested about the fitsrec-changes. I've experimented with them before and always got several failures, so I'm really interested to see if it works out. 👍

@saimn

saimn commented Sep 6, 2016

Copy link
Copy Markdown
Contributor Author

@MSeifert04 - I can probably squash 2 or 3 commits for things that are modified in another commit, but there are a number of commits I would like to keep as they address specific things and contain some information on the change.

As a descriptor it is often called (for each attribute of the column). So
instead of adding each time '_', add it once.
Getting names from dtype.names should be safe and avoids accessing _coldefs.

Also use directly self.names, and change several _coldefs indexing to get the
column before accessing an attribute.
@saimn
saimn force-pushed the fits-table-optims branch from 52bd47b to a7ae89e Compare September 6, 2016 20:25
@saimn

saimn commented Sep 6, 2016

Copy link
Copy Markdown
Contributor Author

Ok, squashed some commits, and added changelog.

@saimn

saimn commented Sep 7, 2016

Copy link
Copy Markdown
Contributor Author

The build failure seems unrelated (something with a samp hub process: https://travis-ci.org/astropy/astropy/jobs/157994577#L2451).

Comment thread CHANGES.rst Outdated

- ``astropy.io.fits``

- Optimized the loading of tables with many columns. [#4985]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "optimized" is a strong word. It's hardly optimal. You can just say "Performance improvements"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, done.

@saimn
saimn force-pushed the fits-table-optims branch from a7ae89e to cf0b815 Compare September 7, 2016 09:47
@saimn
saimn force-pushed the fits-table-optims branch from cf0b815 to 0a58c66 Compare September 7, 2016 09:48
@embray

embray commented Sep 7, 2016

Copy link
Copy Markdown
Member

Great--thanks for putting up with my nitpicks. I think this is good!

@pllim

pllim commented Sep 7, 2016

Copy link
Copy Markdown
Member

Got approval, so merging. Thanks, @saimn !

@pllim
pllim merged commit c18eb28 into astropy:master Sep 7, 2016
@saimn

saimn commented Sep 7, 2016

Copy link
Copy Markdown
Contributor Author

Thanks for merging :)

@saimn
saimn deleted the fits-table-optims branch September 9, 2016 13:31
@taldcroft

Copy link
Copy Markdown
Member

@saimn - I'm not sure if it was exactly this PR, but reading short wide tables is way faster for me now, as in a factor of 8! (Comparing 1.2.1 to current master 1.3.dev16204). This is awesome and therefore closes #4609.

@taldcroft

Copy link
Copy Markdown
Member

Specifically, for this table I get the factor of 8 improvement:

import numpy as np
from astropy.table import Table

ncols = 800
nrows = 10
cols = [np.random.uniform(size=nrows) for _ in xrange(ncols)]
dat = Table(cols)

dat.write('test_speed.fits', overwrite=True)

@saimn

saimn commented Oct 14, 2016

Copy link
Copy Markdown
Contributor Author

@taldcroft - This is typically the use case I was addressing here, tables with a many columns, so I'm glad it is useful to someone else :)

@embray

embray commented Oct 17, 2016

Copy link
Copy Markdown
Member

Thanks @taldcroft for the testimonial--I bet it was this PR!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants