From: Junio C Hamano <hidden> Date: 2021-08-27 16:15:19
Jeff King [off-list ref] writes:
On Thu, Aug 26, 2021 at 09:33:08AM -0700, Junio C Hamano wrote:
quoted
quoted
+ /*
+ * Increase the stdio buffer size for stdout, for the benefit of ref
+ * advertisement writes. We are only allowed to call setvbuf(3) "after
+ * opening a stream and before any other operations have been performed
+ * on it", so let's call it before we have written anything to stdout.
+ */
+ if (setvbuf(stdout, xmalloc(LARGE_PACKET_MAX), _IOFBF,
+ LARGE_PACKET_MAX))
+ die_errno("failed to grow stdout buffer");
Nice to see a comment on the tricky part. I do not think we mind if
we rounded up the allocation size to the next power of two here, but
there probably won't be any measurable benefit for doing so.
I'm a little negative on this part, actually. The commit message claims
it's for consistency across platforms. But I would argue that if your
libc buffer size is sub-optimal, then we shouldn't be sprinkling these
adjustments through the code. Either:
- we should consider it a quality-of-implementation issue, and people
using that libc should push back on their platform to change the
default size; or
- we should fix it consistently and transparently throughout Git by
adjusting std{in,out,err} in common-main, and using fopen()/fdopen()
wrappers to adjust to something more sensible
I agree that it would be ideal if we didn't do setvbuf() at all, the
second best would be to do so at a central place. My "Nice" applied
only to the comment ;-)
I also find the use of LARGE_PACKET_MAX weird here. Is that really the
optimal stdio buffer size? The whole point of this is coalescing _small_
packets into a single write() syscall. So how many small packets fit
into LARGE_PACKET_MAX is comparing apples and oranges.
The sizing is all my fault. The original used 32k threashold and
implemented manual buffering by flushing whenever the accumulated
data exceeded the threashold, as opposed to "if we buffer this new
piece, it would exceed, so let's flush first what we got so far,
which is smaller than the threashold", which I found indefensible in
two ways. The "flush _after_ we go over it" semantics looked iffy,
and 32k was totally out of thin air. As LARGE_PACKET_MAX is the
hard upper limit of each packet that has been with us forever, it
was more defensible than 32k ;-)
But if we are using stdio, I agree that it is much better not to
worry about sizing at all by not doing setvbuf() and leaving it to
libc implementation. They ought to know what works on their
platform the best.
Thanks.
From: Jacob Vosmaer <hidden> Date: 2021-08-31 09:35:23
Changes compared to v2:
- remove setvbuf call
- add packet_fwrite_fmt
- add packet_fflush
Non-changes:
- no ferror calls because those are for reads, not for writes
Thanks for the reactions everyone. I agree that packet_fwrite_fmt
simplifies the patch nicely. Jeff, I hope I have given you credit
in an appropriate way, let me know if you want me to change something
there.
Regarding setvbuf: I have found out that GNU coreutils has a utility
called stdbuf that lets you modify the stdout buffer size at runtime
using some LD_PRELOAD hack so we can use that in Gitaly. I don't
think this is the best outcome for users, we ought to give them a
good default instead of expecting them to invoke git-upload-pack
as 'stdbuf -o 64K git-upload-pack'. But I can't judge the impact
of globally changing the stdout buffer size for Git so I'll settle
for having to use stdbuf.
Jacob Vosmaer (2):
pkt-line: add packet_fwrite and packet_fwrite_fmt
upload-pack: use stdio in send_ref callbacks
cache.h | 2 ++
ls-refs.c | 4 +++-
pkt-line.c | 30 ++++++++++++++++++++++++++++++
pkt-line.h | 8 ++++++++
upload-pack.c | 8 +++++---
write-or-die.c | 12 ++++++++++++
6 files changed, 60 insertions(+), 4 deletions(-)
--
2.32.0
From: Jacob Vosmaer <hidden> Date: 2021-08-31 09:35:29
This adds three new functions to pkt-line.c: packet_fwrite,
packet_fwrite_fmt and packet_fflush. Besides writing a pktline flush
packet, packet_fflush also flushes the stdio buffer of the stream.
Helped-by: Patrick Steinhardt [off-list ref]
Helped-by: Jeff King [off-list ref]
Signed-off-by: Jacob Vosmaer <redacted>
---
cache.h | 2 ++
pkt-line.c | 37 +++++++++++++++++++++++++++++++++++++
pkt-line.h | 11 +++++++++++
write-or-die.c | 12 ++++++++++++
4 files changed, 62 insertions(+)
@@ -35,6 +35,17 @@ int packet_write_fmt_gently(int fd, const char *fmt, ...) __attribute__((formatintwrite_packetized_from_fd_no_flush(intfd_in,intfd_out);intwrite_packetized_from_buf_no_flush(constchar*src_in,size_tlen,intfd_out);+/*+*Stdioversionsofpacket_writefunctions.Whenmixingthesewithfd+*basedfunctions,takecaretocallfflushorpacket_fflushbefore+*doingfdwritesorclosingthefd.+*/+voidpacket_fwrite(FILE*f,constchar*buf,size_tsize);+voidpacket_fwrite_fmt(FILE*f,constchar*fmt,...)__attribute__((format(printf,2,3)));++/* packet_fflush writes a flush packet and flushes the stdio buffer of f */+voidpacket_fflush(FILE*f);+/**Readapacketizedlineintothebuffer,whichmustbeatleastsizebytes*long.Thereturnvaluespecifiesthenumberofbytesreadintothebuffer.
From: Jacob Vosmaer <hidden> Date: 2021-08-31 09:35:30
In both protocol v0 and v2, upload-pack writes one pktline packet per
advertised ref to stdout. That means one or two write(2) syscalls per
ref. This is problematic if these writes become network sends with
high overhead.
This commit changes both send_ref callbacks to use buffered IO using
stdio.
To give an example of the impact: I set up a single-threaded loop that
calls ls-remote (with HTTP and protocol v2) on a local GitLab
instance, on a repository with 11K refs. When I switch from Git
v2.32.0 to this patch, I see a 40% reduction in CPU time for Git, and
65% for Gitaly (GitLab's Git RPC service).
So using buffered IO not only saves syscalls in upload-pack, it also
saves time in things that consume upload-pack's output.
Helped-by: Jeff King [off-list ref]
Signed-off-by: Jacob Vosmaer <redacted>
---
ls-refs.c | 4 ++--
upload-pack.c | 11 ++++++++---
2 files changed, 10 insertions(+), 5 deletions(-)
From: Jeff King <hidden> Date: 2021-08-31 10:25:34
On Tue, Aug 31, 2021 at 11:34:42AM +0200, Jacob Vosmaer wrote:
Thanks for the reactions everyone. I agree that packet_fwrite_fmt
simplifies the patch nicely. Jeff, I hope I have given you credit
in an appropriate way, let me know if you want me to change something
there.
What you did looks fine.
Overall the series looks much nicer, and I don't have any real
complaints. I do think it would be nice to take the packet_writer
interface further (letting it replace the static buf, and use stdio
handles, and using it throughout upload-pack). But this is a strict
improvement, so we can do that other refactoring later.
Regarding setvbuf: I have found out that GNU coreutils has a utility
called stdbuf that lets you modify the stdout buffer size at runtime
using some LD_PRELOAD hack so we can use that in Gitaly. I don't
think this is the best outcome for users, we ought to give them a
good default instead of expecting them to invoke git-upload-pack
as 'stdbuf -o 64K git-upload-pack'. But I can't judge the impact
of globally changing the stdout buffer size for Git so I'll settle
for having to use stdbuf.
Does the 64k buffer actually improve things? Here are the timings I get
on a repo with ~1M refs (it's linux.git with one ref per commit). "git"
is current unbuffered version, and "git.compile" is master with your
patches on top:
$ hyperfine -i 'git upload-pack .' 'git.compile upload-pack .' 'stdbuf -o 64K git.compile upload-pack .'
Benchmark #1: git upload-pack .
Time (mean ± σ): 948.6 ms ± 7.3 ms [User: 840.8 ms, System: 107.8 ms]
Range (min … max): 937.7 ms … 961.1 ms 10 runs
Warning: Ignoring non-zero exit code.
Benchmark #2: git.compile upload-pack .
Time (mean ± σ): 867.3 ms ± 6.8 ms [User: 821.5 ms, System: 45.7 ms]
Range (min … max): 859.7 ms … 883.0 ms 10 runs
Warning: Ignoring non-zero exit code.
Benchmark #3: stdbuf -o 64K git.compile upload-pack .
Time (mean ± σ): 861.1 ms ± 8.2 ms [User: 815.5 ms, System: 45.6 ms]
Range (min … max): 846.1 ms … 872.0 ms 10 runs
Warning: Ignoring non-zero exit code.
Summary
'stdbuf -o 64K git.compile upload-pack .' ran
1.01 ± 0.01 times faster than 'git.compile upload-pack .'
1.10 ± 0.01 times faster than 'git upload-pack .'
This is on a glibc system, so the default buffers should be 4k. It
doesn't appear to make any difference (there's a slight improvement, but
well within the noise, and I had other runs where it did worse).
By the way, if you really want to speed things up, try this:
$ hyperfine -i 'git.compile upload-pack .' 'GIT_REF_PARANOIA=1 git.compile upload-pack .'
Benchmark #1: git.compile upload-pack .
Time (mean ± σ): 855.4 ms ± 5.8 ms [User: 803.4 ms, System: 52.0 ms]
Range (min … max): 848.7 ms … 869.5 ms 10 runs
Warning: Ignoring non-zero exit code.
Benchmark #2: GIT_REF_PARANOIA=1 git.compile upload-pack .
Time (mean ± σ): 394.4 ms ± 3.0 ms [User: 357.9 ms, System: 36.4 ms]
Range (min … max): 390.6 ms … 400.3 ms 10 runs
Warning: Ignoring non-zero exit code.
Summary
'GIT_REF_PARANOIA=1 git.compile upload-pack .' ran
2.17 ± 0.02 times faster than 'git.compile upload-pack .'
It's not exactly the intended use of that environment variable, but its
side effect is that we do not call has_object_file() on each ref tip.
-Peff
One small oddity I noticed. The definition of fwrite is
fwrite(ptr, size, nmemb, strea), where we write "nmemb" items of "size"
bytes each. I'd argue we're writing "count" single bytes, so it should
be:
if (fwrite(buf, 1, count, f) != count)
This matters a lot for fread(), where any read shorter than "count"
(e.g., due to EOF) would return "0" rather than a partial result. But I
have a hard time imagining an implementation of fwrite() where the
distinction would matter. And grepping around, we seem to have both
forms in our code base already. So it's probably fine.
-Peff
From: Jacob Vosmaer <hidden> Date: 2021-08-31 13:08:41
On Tue, Aug 31, 2021 at 12:25 PM Jeff King [off-list ref] wrote:
I do think it would be nice to take the packet_writer
interface further (letting it replace the static buf, and use stdio
handles, and using it throughout upload-pack).
I would like that too, for the sake of neatness and general
performance, but I don't have the time to take on a larger project
like that at the moment.
Does the 64k buffer actually improve things? Here are the timings I get
on a repo with ~1M refs (it's linux.git with one ref per commit).
Thanks for challenging that. I have a repeatable benchmark where it
matters, because each write syscall wakes up a chain of proxies
between the user and git-upload-pack. Larger buffers means fewer
wake-ups. But then I tried to simplify my example by having sshd as
the only intermediary, and in that experiment 64K buffers were not
better than 4K buffers. I think that goes to show that picking a good
buffer size is hard, and we'd be better off picking one specifically
for Gitaly (and GitLab) that works with our stack.
Summary
'GIT_REF_PARANOIA=1 git.compile upload-pack .' ran
2.17 ± 0.02 times faster than 'git.compile upload-pack .'
It's not exactly the intended use of that environment variable, but its
side effect is that we do not call has_object_file() on each ref tip.
That is nice to know, but as a user of Git I don't know when it is or
is not safe to skip those has_object_file() calls. If it's safe to
skip them then Git should skip them always. If not, then I will err on
the side of caution and keep the checks.
Jacob
From: Jacob Vosmaer <hidden> Date: 2021-08-31 17:44:15
On Tue, Aug 31, 2021 at 3:08 PM Jacob Vosmaer [off-list ref] wrote:
On Tue, Aug 31, 2021 at 12:25 PM Jeff King [off-list ref] wrote:
quoted
I do think it would be nice to take the packet_writer
interface further (letting it replace the static buf, and use stdio
handles, and using it throughout upload-pack).
I would like that too, for the sake of neatness and general
performance, but I don't have the time to take on a larger project
like that at the moment.
I gave solving the problem with packet_writer a couple of hours today.
The diff gets too big, and I have too little confidence I'm not
introducing deadlocks. This really is more work than I can chew off
right now. Sorry!
Jacob