Skip to content

Why looping through hdus at pos=2 in fits files is much slower than looping through hdus at pos=1? #12491

Open

Description

@Nestak2

Description

I am looping through the hdus of astro fits files I am analyzing. When I loop through the hdus at position 1 (hdu=1) with astropy.table.read the time for that is reasonably short. But if I do the same for the hdus at position 2 (hdu=2) the time is 10 times slower, despite that the size of hdu=2 is smaller than for hdu=1. The tables/hdus at position 2 are smaller (~27 KB) than the tables at position 1 (~130 kB). Further, tables at position 1 have shape 2833 rows * 8 columns and tables at position 2 have shape 1 row * 126 columns. The time matters to me, because I need to loop through tens of thousands of files eventually, so in the magnitude of hours.

Example file can be this one: https://data.sdss.org/sas/dr16/eboss/spectro/redux/v5_13_0/spectra/lite/3699/spec-3699-55517-0420.fits

Expected behavior

Looping should be the same or maybe faster for hdu=2.

Actual behavior

Looping through hdu=2 is much slower (~10 times).

Steps to Reproduce

To reproduce you can do

from astropy.table import Table

for file_name in fits_files_list:
    table1 = Table.read(file_name, hdu=1)

# fits_files_list is a list of containing the file paths to fits files. If you want to reproduce my code you can download an example from here:
# https://data.sdss.org/sas/dr16/eboss/spectro/redux/v5_13_0/spectra/lite/3699/spec-3699-55517-0420.fits
# this is the sort of files I am working with now, they all have the same structure

Change then hdu=1 to hdu=2, keep track of the execution time and note that the second time it is ~10 times slower.

System Details

The details to my system/environment are:
Linux-5.11.0-40-generic-x86_64-with-glibc2.10
Python 3.8.5 (default, Sep 4 2020, 07:30:14)
[GCC 7.3.0]
Numpy 1.19.1
astropy 3.2.3

I have to erfa or scipy installed.

How can I speed up the looping through hdu=2? Why is it 10 times slower? Are there any work around? Tnx

Activity

  1. github-actions commented on Nov 18, 2021

    @github-actions
    Contributor

    Welcome to Astropy 👋 and thank you for your first issue!

    A project member will respond to you as soon as possible; in the meantime, please double-check the guidelines for submitting issues and make sure you've provided the requested details.

    GitHub issues in the Astropy repository are used to track bug reports and feature requests; If your issue poses a question about how to use Astropy, please instead raise your question in the Astropy Discourse user forum and close this issue.

    If you feel that this issue has not been responded to in a timely manner, please leave a comment mentioning our software support engineer @embray, or send a message directly to the development mailing list. If the issue is urgent or sensitive in nature (e.g., a security vulnerability) please send an e-mail directly to the private e-mail [email protected].

  2. pllim commented on Nov 18, 2021

    @pllim
    Member

    Did you try to profile the code to see where the bottleneck is for your use case? Does it help if you pass in memmap=True?

    As you can see in https://github.com/astropy/astropy/blob/main/astropy/io/fits/connect.py , there are a bunch of stuff going on, so profiling your code would be the best way to figure out why.

  3. embray commented on Nov 18, 2021

    @embray
    Member

    I looked at the example file in question, and although the data in hdu=1 is ;arger, that table contains only 8 columns, whereas hdu=2 has 236 columns which is quite a lot. The time of reading the table seems to scale linearly in this case with the number of columns. In both cases they are very small tables, so the time of loading these tables is actually dominated by parsing the column data from the header, compared to actually reading the data.

    We could try profiling the code and see if there any pain points slowing this down, but regardless this is an issue specific to these files, and the large number of columns in those tables.

  4. embray commented on Nov 18, 2021

    @embray
    Member

    From a cursory look there are a couple obvious problems. One is that when loading data from a binary table it actually goes through ColDefs.__init__ twice, redundantly. This could and should be fixed. The other issue is, I as suspected, that just initializing the ColDefs at all is slow, in both cases. A large portion of that time is dominated by either reading header keywords (not a problem usually unless there are hundreds of columns, and thus N*hundreds of header keywords to read), and constructing the Column objects themselves. I think in both cases there are opportunities for optimization, but it will require a closer look.

  5. saimn commented on Nov 18, 2021

    @saimn
    Contributor

    Indeed the number of columns must be the reason reading this HDU is slower. I'm not sure there are easy optimizations there (since I remember struggling with this a long time ago, #4985 :)). One alternative way however would be to use the io.fits API instead of Table, since there is probably a non-negligible cost in the BinTableHDU to Table conversion.

  6. Nestak2 commented on Nov 18, 2021

    @Nestak2
    Author

    Thanks for all the answers! @embray @saimn I actually don't need data from each of the 126 columns in hdu=2, I only need ~10 columns from that table. Is there some hack in that connection that I could use?

  7. embray commented on Nov 19, 2021

    @embray
    Member

    @saimn What I found in investigating this is that using io.fits is no faster. Converting to Table incurs some overhead but not as much.

    What I found was that in _TableLikeHDU._get_data, a ColDefs for the table gets constructed twice. The first time is when it calls self.columns where it constructs the ColDefs from the header. It then uses this to determine the appropriate dtype, as well as some other processing of the data format.

    Then on this line it views the array as a FITS_rec (a class that is pretty vestigial at this point, but will take some effort to get rid of). This results in constructing a new ColDefs object, but this time from the dtype instead of the header.

    This redundancy has significant overhead for a case like this, and should be done away with, though I'm not sure how yet.

    Both of the methods used here for constructing ColDefs (from the header, and then from the dtype) have opportunities for improvement as well.

  8. embray commented on Nov 19, 2021

    @embray
    Member

    @Nestak2 In order to know where the columns are in a table the column information has to be parsed anyways. The only way you could work around that is if you know each table has the exact same layout, then I guess you could do something.

    Though that question raises for me one possibility of optimizing cases like this.

    If you are looping over a large number of similar files (which is not such an uncommon case), and all the files contain tables with the exact same structure, we could have a system for caching table column structures, e.g. based on the table keywords from the header.

    Do you know for sure in your case that all/most of the tables have the exact same columns?

  9. embray commented on Nov 19, 2021

    @embray
    Member

    Another interesting thing I found while examining the performance here is that lookup of header keyword values is actually being largely dominated by, of all things, the astropy config system. There are various config settings that are checked when parsing headers, some of which will get checked for every single keyword.

    On some level this is necessary since astropy configs are supposed to be reloadable at runtime. But I wonder if for parsing headers it would make sense to allow config changes between reading individual values from the same header.

  10. pllim commented on Nov 19, 2021

    @pllim
    Member

    Where exactly in the code is config heavily used -- table or io.fits?

    @bsipocz , @saimn , and I were just discussing on how we shouldn't use it so much the other day. We should only use it for something that is "set and forget" for a session. It is not a replacement for **kwargs.

  11. embray commented on Nov 19, 2021

    @embray
    Member

    You can manually reset a specific config variable, but there's nothing "magic" where it changes automatically if you edit the config file at runtime. You still have to call conf.reset('option')

  12. pllim commented on Nov 19, 2021

    @pllim
    Member

    I am not saying there is magic. I am wondering if we can stop using config for better performance, but this is FITS, so maybe not...

  13. bsipocz commented on Nov 19, 2021

    @bsipocz
    Member

    Another interesting thing I found while examining the performance here is that lookup of header keyword values is actually being largely dominated by, of all things, the astropy config system. There are various config settings that are checked when parsing headers, some of which will get checked for every single keyword.

    That's a good test and reminder that we should then indeed use the config system to set the defaults in init, but not to directly set the default values for method kwargs. (Though I don't think it should be an issue for astroquery where we end up querying a remote server, in no way should any of those methods be limited by the config system)

  14. Nestak2 commented on Nov 22, 2021

    @Nestak2
    Author

    Do you know for sure in your case that all/most of the tables have the exact same columns?

    @embray Thanks, I think that most if not all tables have the exact same columns, but I am not 100% sure, because I am just starting to use them as a PhD student. More precisely, the files I am working with are fits-files from the SDSS archive, all of them containing spectral info, limited to only QSO objects and part of the eBOSS survey. Below I have the top 5 URLs from ~20000 or even more, that I would need to work with, if you want to check them yourself.

    http://dr16.sdss.org/sas/dr16/sdss/spectro/redux/v5_13_0/spectra/lite/7572/spec-7572-56944-0110.fits
    http://dr16.sdss.org/sas/dr16/sdss/spectro/redux/v5_13_0/spectra/lite/7572/spec-7572-56944-0118.fits
    http://dr16.sdss.org/sas/dr16/sdss/spectro/redux/v5_13_0/spectra/lite/7572/spec-7572-56944-0134.fits
    http://dr16.sdss.org/sas/dr16/sdss/spectro/redux/v5_13_0/spectra/lite/7572/spec-7572-56944-0144.fits
    http://dr16.sdss.org/sas/dr16/sdss/spectro/redux/v5_13_0/spectra/lite/7572/spec-7572-56944-0148.fits
    

    If you want to check them in another way - I query the files to work with with the SQL query tool of Cas jobs using this SQL code:

    SELECT top 15 
        s.z, s.zerr, s.zWarning, s.subclass, s.spectroFlux_u, s.spectroFlux_g, s.spectroFlux_r, s.spectroFlux_i, s.spectroFlux_z, dbo.fGetUrlFitsSpectrum(s.specobjid) as url,
        p.psfFlux_u	, p.psfFlux_g, p.psfFlux_r, p.psfFlux_i, p.psfFlux_z, p.ObjID, p.ra, p.dec
      
    FROM photoObj p
    JOIN specObj s ON s.bestObjID = p.objID
             
    WHERE
        s.class = 'qso' 
        and s.zWarning = 0 
        and s.z between 1.0 and 4.0
        and s.programname='eboss'
    

    Any ideas how to optimize the time for looping through tables with exact same columns? Tnx

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions