Skip to content

ftplib and sendfile() #57773

Open
Open

Description

@giampaolo
BPO 13564
Nosy @pitrou, @giampaolo, @merwok
Dependencies
  • bpo-17552: Add a new socket.sendfile() method
  • Files
  • ftplib-sendfile.patch: initial draft
  • ftplib-sendfile2.patch: nobytes = fiilesize
  • ftplib-sendfile3.patch: use poll(); sock timeout; look for EMFILE;
  • ftplib-sendfile4.patch: do not set non-blocking; factor out support functions; improve test
  • ftplib-sendfile5.patch: uses socket.sendfile()
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = 'https://github.com/giampaolo'
    closed_at = None
    created_at = <Date 2011-12-09.06:56:06.908>
    labels = ['3.7', 'performance']
    title = 'ftplib and sendfile()'
    updated_at = <Date 2016-09-08.22:54:07.941>
    user = 'https://github.com/giampaolo'

    bugs.python.org fields:

    activity = <Date 2016-09-08.22:54:07.941>
    actor = 'christian.heimes'
    assignee = 'giampaolo.rodola'
    closed = False
    closed_date = None
    closer = None
    components = []
    creation = <Date 2011-12-09.06:56:06.908>
    creator = 'giampaolo.rodola'
    dependencies = ['17552']
    files = ['23890', '29336', '29351', '29357', '35568']
    hgrepos = []
    issue_num = 13564
    keywords = ['patch']
    message_count = 32.0
    messages = ['149076', '149170', '149237', '183599', '183601', '183602', '183603', '183606', '183607', '183609', '183622', '183637', '183654', '183655', '183666', '183669', '183670', '183673', '183680', '183754', '183755', '183814', '183827', '183828', '183831', '183832', '183834', '183835', '184881', '184882', '185293', '220261']
    nosy_count = 4.0
    nosy_names = ['pitrou', 'giampaolo.rodola', 'eric.araujo', 'rosslagerwall']
    pr_nums = []
    priority = 'normal'
    resolution = None
    stage = 'patch review'
    status = 'open'
    superseder = None
    type = 'performance'
    url = 'https://bugs.python.org/issue13564'
    versions = ['Python 3.6', 'Python 3.7']

    Activity

    1. giampaolo commented on Dec 9, 2011

      @giampaolo
      ContributorAuthor

      In attachment.
      This is actually just an excuse to store the patch somewhere and possibly collect opinions as I don't really think this should go in because:

      • it's UNIX only
      • as such, deciding whether using sendfile() should probably be done silently (no explicit argument)
      • on the other hand, I don't think it's safe to decide this automatically because:
        • the input fd should be a regular file and it's not clear how to determine this beforehand
        • in case of disconnection OSError is raised instead of socket.error (minor backward compatibility issue)
    2. merwok commented on Dec 10, 2011

      @merwok
      Member

      deciding whether using sendfile() should probably be done silently (no explicit argument)
      As an optimization taking advantage from OS support, I think this should be automatic too. But if there are too many issues, then explicit argument sounds better.

      the input fd should be a regular file and it's not clear how to determine this beforehand
      Calling some function like os.stat that only works with real files? (not sure os.stat is the right function, just giving an idea)

      > in case of disconnection OSError is raised instead of socket.error
      PEP 3151!
      >>> socket.error, OSError, IOError
      (<class 'OSError'>, <class 'OSError'>, <class 'OSError'>)

      :)

    3. pitrou commented on Dec 11, 2011

      @pitrou
      Member

      os.fstat wouldn't work since it succeeds with non-"regular" files, e.g. standard I/O:

      >>> os.fstat(0)
      posix.stat_result(st_mode=8592, st_ino=5, st_dev=11, st_nlink=1, st_uid=500, st_gid=5, st_size=0, st_atime=1323629303, st_mtime=1323629303, st_ctime=1323628616)

      I think the best solution is to call sendfile() and catch OSError, then fallback on the generic loop. However, you must also guard against fileno() failing:

      >>> io.BytesIO().fileno()
      Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
      io.UnsupportedOperation: fileno
    4. neologix commented on Mar 6, 2013

      neologixmannequin
      Mannequin

      Here's the result of a benchmark sending a 1GB file over a Gb/s ethernet network:
      vanilla:
      real 0m9.446s
      user 0m0.493s
      sys 0m1.425s

      sendfile:
      real 0m9.143s
      user 0m0.055s
      sys 0m0.986s

      The total time doesn't vary much (the reduction above is just jitter).
      But it reduces user+sys time by almost a factor of 2.

      Note that is changed Giampaolo's patch to call sendfile on the whole file, not by block.

    5. giampaolo commented on Mar 6, 2013

      @giampaolo
      ContributorAuthor

      Note that is changed Giampaolo's patch to call sendfile
      on the whole file, not by block.

      That's not compatible across POSIX platforms.

    6. neologix commented on Mar 6, 2013

      neologixmannequin
      Mannequin

      That's not compatible across POSIX platforms.

      What do you mean ?
      I juste call fstat() before calling senfile() to find out the file
      size, and pass it as count.

    7. giampaolo commented on Mar 6, 2013

      @giampaolo
      ContributorAuthor

      Ah ok, I misinterpreted what you wrote then.
      Generally speaking though, you don't need to know the file size: you just decide a blocksize (>= 65536 is usually ok) and use sendfile() as you use send().

    8. neologix commented on Mar 6, 2013

      neologixmannequin
      Mannequin

      Ah ok, I misinterpreted what you wrote then.
      Generally speaking though, you don't need to know the file size: you just decide a blocksize (>= 65536 is usually ok) and use sendfile() as you use send().

      But then you make much more syscalls than what's necessary, and your
      heuristic for the block size is never going to match the choice the
      kernel makes.

      Anyway, here are the numbers, do you think it's interesting to merge
      (I mean, you're the Python ftp expert ;-) ?

    9. giampaolo commented on Mar 6, 2013

      @giampaolo
      ContributorAuthor

      Specifying a big blocksize doesn't mean the transfer will be faster.
      send/sendfile won't send more than a certain amount of bytes anyways.
      If I'm not mistaken I recall from previous benchmarks that after a certain point (131072 or something) increasing the blocksize results in equal or even worse performances.

      Another thing I don't like is that by doing so you implicitly assume that the file is "fstat-eable". I don't know if there are cases where it's not, but the less assumptions we do the better.

      Note: I'm sure that for both send() and sendfile() blocksize>=65536 is faster than blocksize=8192 (the current default) so it probably makes sense to change that (I'll file a separate issue).

    10. neologix commented on Mar 6, 2013

      neologixmannequin
      Mannequin

      Specifying a big blocksize doesn't mean the transfer will be faster.
      send/sendfile won't send more than a certain amount of bytes anyways.

      The transfer won't be faster mainly because it's really I/O bound.
      But it will use less CPU, only because you're making less syscalls.

      If I'm not mistaken I recall from previous benchmarks that after a certain point (131072 or something) increasing the blocksize results in equal or even worse performances.

      I can perfectly believe this for a send loop, maybe because you're
      exceeding the socket buffer size, or because your working set doesn't
      fit into caches anymore, etc.
      But for sendfile(), I don't see how calling it repeatedly could not be
      slower than calling it once with the overall size: that's how netperf
      and vsftpd use it, and probably others.

      Another thing I don't like is that by doing so you implicitly assume that the file is "fstat-eable". I don't know if there are cases where it's not, but the less assumptions we do the better.

      Well, the file must be mmap-able, so I doubt fstat() is the biggest concern...

    11. giampaolo commented on Mar 6, 2013

      @giampaolo
      ContributorAuthor

      The transfer won't be faster mainly because it's really I/O bound.
      But it will use less CPU, only because you're making less syscalls.

      Have you actually measured this?

    12. neologix commented on Mar 7, 2013

      neologixmannequin
      Mannequin

      > The transfer won't be faster mainly because it's really I/O bound.
      > But it will use less CPU, only because you're making less syscalls.

      Have you actually measured this?

      """
      vanilla over Gb/s:
      real 0m9.035s
      user 0m0.523s
      sys 0m1.412s

      block-sendfile over Gb/s:
      real 0m9.683s
      user 0m0.253s
      sys 0m1.212s

      full-sendfile over Gb/s:
      real 0m9.014s
      user 0m0.059s
      sys 0m1.000s
      """

      As you can see, the throughput doesn't vary (the difference in "real
      time" is just part of the variance).
      However, the CPU usage (user+sys) is less for block-sendfile than send
      loop, and less for full-sendfile than block-sendfile.

      """
      vanilla over loopback:
      real 0m3.200s
      user 0m0.541s
      sys 0m0.702s

      block-sendfile over loopback:
      real 0m2.713s
      user 0m0.248s
      sys 0m0.197s

      full-sendfile over loopback:
      real 0m1.718s
      user 0m0.055s
      sys 0m0.082s
      """

      Same thing for loopback, except that here, zero-copy makes a
      difference on the throughput because we're not I/O bound, but really
      CPU/memory bound (and here sendfile of the complete file really
      outperforms block-sendfile).

      I don't have access to a 10Gb/s network, but basic math hints that
      sendfile could make a difference on the overall throughput.

    13. giampaolo commented on Mar 7, 2013

      @giampaolo
      ContributorAuthor

      It seems you're right, sorry. We need to take that into account then.

      In the meantime I rewrote the original patch and got rid of the "use_sendfile" explicit argument in order to attempt to use sendfile() by default and fall back on using send() if bytes sent were 0.

      TSL_FTP related changes are still left out for now as I'm planning a little refactoring first.

    14. neologix commented on Mar 7, 2013

      neologixmannequin
      Mannequin

      In the meantime I rewrote the original patch and got rid of the "use_sendfile" explicit argument in order to attempt to use sendfile() by default and fall back on using send() if bytes sent were 0.

      """
      # block until socket is writable
      select.select([], [sockno], [])
      """

      I don't get it, why do you use select?

    15. 11 remaining items

    16. pitrou commented on Mar 9, 2013

      @pitrou
      Member

      Because otherwise sendfile() fails with EAGAIN many times before
      sending any actual data.

      EAGAIN on a blocking fd? Is it documented somewhere?
      The Linux man page for sendfile() says:

         EAGAIN Nonblocking I/O has been selected using O_NONBLOCK and the
      

      write would block.

      FreeBSD apparently says something similar:

       [EAGAIN]		The socket is marked for non-blocking I/O and not all
      		data was sent due to the socket buffer being filled.
      		If specified, the number of bytes successfully sent
      		will be returned in \*sbytes.
      
    17. giampaolo commented on Mar 9, 2013

      @giampaolo
      ContributorAuthor

      I see. Well, what I'm experiencing right now if I remove the select() / poll() call is a sequence of EAGAIN errors alternated by successful sendfile() calls.
      Either the man page is wrong or I'm missing something.

    18. pitrou commented on Mar 9, 2013

      @pitrou
      Member

      I see. Well, what I'm experiencing right now if I remove the
      select() / poll() call is a sequence of EAGAIN errors alternated by
      successful sendfile() calls.
      Either the man page is wrong or I'm missing something.

      Weird. I guess it would be nice to dig a bit more :-)

    19. giampaolo commented on Mar 21, 2013

      @giampaolo
      ContributorAuthor

      After digging a bit further it seems EAGAIN occurs in case a timeout was previously set against the socket as in ftplib.FTP(..., timeout=2) (at least on Linux, FWICT).

      As such, we can debate whether avoid using select/poll if timeout was not set.
      I'll that a look at the man pages of the other POSIX platforms and figure whether EAGAIN is interpreted as on Linux.

      Other than that, the patch is reasonably ok to me and can be committed as-is and blocksize argument tuning can be discussed in a separate ticket.

    20. pitrou commented on Mar 21, 2013

      @pitrou
      Member

      After digging a bit further it seems EAGAIN occurs in case a timeout was previously
      set against the socket as in ftplib.FTP(..., timeout=2) (at least on Linux, FWICT).

      Ah, indeed. That's because socket timeout makes the underlying fd non-blocking.
      Which means there probably should be a higher-level sendfile() facility for sockets, taking into account the socket timeout...

    21. giampaolo commented on Mar 26, 2013

      @giampaolo
      ContributorAuthor

      I created bpo-17552 in order to discuss about socket.sendfile() addition.

    22. giampaolo commented on Jun 11, 2014

      @giampaolo
      ContributorAuthor

      Updated patch which uses the newly added socket.sendfile() method (bpo-17552).

    23. self-assigned this
      on Jun 11, 2014
    24. added
      performancePerformance or resource usage
      and removed
      type-featureA feature request or enhancement
      on Jun 11, 2014
    25. transferred this issue fromon Apr 10, 2022
    26. added
      stdlibStandard Library Python modules in the Lib/ directory
      3.12only security fixes
      and removed on Sep 11, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Assignees

    Labels

    3.12only security fixesperformancePerformance or resource usagestdlibStandard Library Python modules in the Lib/ directory

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions