All of lore.kernel.org
 help / color / mirror / Atom feed
From: Michael Haggerty <mhagger@alum.mit.edu>
To: Jeff King <peff@peff.net>,
	Remi Galan Alfonso <remi.galan-alfonso@ensimag.grenoble-inp.fr>
Cc: William Duclot <william.duclot@ensimag.grenoble-inp.fr>,
	git@vger.kernel.org,
	simon rabourg <simon.rabourg@ensimag.grenoble-inp.fr>,
	francois beutin <francois.beutin@ensimag.grenoble-inp.fr>,
	antoine queru <antoine.queru@ensimag.grenoble-inp.fr>,
	matthieu moy <matthieu.moy@grenoble-inp.fr>
Subject: Re: [PATCH 0/2] strbuf: improve API
Date: Thu, 2 Jun 2016 13:11:56 +0200	[thread overview]
Message-ID: <5750147C.5060609@alum.mit.edu> (raw)
In-Reply-To: <20160601210713.GA18118@sigill.intra.peff.net>

On 06/01/2016 11:07 PM, Jeff King wrote:
> On Wed, Jun 01, 2016 at 03:42:18AM -0400, Jeff King wrote:
> 
>> I have no idea if those ideas would work. But I wouldn't want to start
>> looking into either of them without some idea of how much time we're
>> actually spending on strbuf mallocs (or how much time we would spend if
>> strbufs were used in some proposed sites).
> 
> So I tried to come up with some numbers.
> 
> Here's an utterly silly use of strbufs, but one that I think should
> over-emphasize the effect of any improvements we make:
> 
> diff --git a/Makefile b/Makefile
> index 7a0551a..72b968a 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -579,6 +579,7 @@ PROGRAM_OBJS += shell.o
>  PROGRAM_OBJS += show-index.o
>  PROGRAM_OBJS += upload-pack.o
>  PROGRAM_OBJS += remote-testsvn.o
> +PROGRAM_OBJS += foo.o
>  
>  # Binary suffix, set to .exe for Windows builds
>  X =
> diff --git a/foo.c b/foo.c
> index e69de29..b62dd97 100644
> --- a/foo.c
> +++ b/foo.c
> @@ -0,0 +1,18 @@
> +#include "git-compat-util.h"
> +#include "strbuf.h"
> +
> +int main(void)
> +{
> +	const char *str = "this is a string that we'll repeatedly insert";
> +	size_t len = strlen(str);
> +
> +	int i;
> +	for (i = 0; i < 1000000; i++) {
> +		struct strbuf buf = STRBUF_INIT;
> +		int j;
> +		for (j = 0; j < 500; j++)
> +			strbuf_add(&buf, str, len);
> +		strbuf_release(&buf);
> +	}
> +	return 0;
> +}

Thanks for generating actual data.

Your benchmark could do two things to stress malloc()/free()
even more. I'm not claiming that this makes the benchmark more typical
of real-world use, but it maybe gets us closer to the theoretical upper
limit on improvement.

1. Since strbuf_grow() allocates new space in geometrically increasing
sizes, the number of mallocs needed to do N strbuf_add()s increases only
like log(N). So decreasing the "500" would increase the fraction of the
time spent on allocations.

2. Since the size of the string being appended increases the time spent
copying bytes, without appreciably changing the number of allocations
(also because of geometric growth), decreasing the length of the string
would also increase the fraction of the time spent on allocations.

I put those together as options, programmed several variants of the
string-concatenating loop, and added a perf script to run them; you can
see the full patch here:


https://github.com/mhagger/git/commit/b417935a4425e0f2bf62e59a924dc652bb2eae0c

The guts look like this:

> 	int j;
> 	if (variant == 0) {
> 		/* Use buffer allocated a single time */
> 		char *buf = big_constant_lifetime_buf;
> 
> 		for (j = 0; j < reps; j++)
> 			strcpy(buf + j * len, str);
> 	} else if (variant == 1) {
> 		/* One correct-sized buffer malloc per iteration */
> 		char *buf = xmalloc(reps * len + 1);
> 
> 		for (j = 0; j < reps; j++)
> 			strcpy(buf + j * len, str);
> 
> 		free(buf);
> 	} else if (variant == 2) {
> 		/* Conventional use of strbuf */
> 		struct strbuf buf = STRBUF_INIT;
> 
> 		for (j = 0; j < reps; j++)
> 			strbuf_add(&buf, str, len);
> 
> 		strbuf_release(&buf);
> 	} else if (variant == 3) {
> 		/* strbuf initialized to correct size */
> 		struct strbuf buf;
> 		strbuf_init(&buf, reps * len);
> 
> 		for (j = 0; j < reps; j++)
> 			strbuf_add(&buf, str, len);
> 
> 		strbuf_release(&buf);
> 	} else if (variant == 4) {
> 		/*
> 		 * Simulated fixed strbuf with correct size.
> 		 * This code only works because we know how
> 		 * strbuf works internally, namely that it
> 		 * will never realloc() or free() the buffer
> 		 * that we attach to it.
> 		 */
> 		struct strbuf buf = STRBUF_INIT;
> 		strbuf_attach(&buf, big_constant_lifetime_buf, 0, reps * len + 1);
> 
> 		for (j = 0; j < reps; j++)
> 			strbuf_add(&buf, str, len);
> 
> 		/* No strbuf_release() here! */
> 	}

I ran this for a short string ("short") and a long string ("this is a
string that we will repeatedly insert"), and also concatenating the
string 5, 20, or 500 times. The number of loops around the whole program
is normalized to make the total number of concatenations approximately
constant. Here are the full results:


                               time (s)
Test                   0      1      2      3      4
----------------------------------------------------
5 short strings     1.64   3.37   8.72   6.08   3.65
20 short strings    1.72   2.12   5.43   4.01   3.39
500 short strings   1.62   1.61   3.36   3.26   3.10
5 long strings      2.08   6.64  13.09   8.50   3.79
20 long strings     2.16   3.33   7.03   4.72   3.55
500 long strings    2.04   2.10   3.61   3.33   3.26


Column 0 is approximately the "bare metal" approach, with a
pre-allocated buffer and no strbuf overhead.

Column 1 is like column 0, except allocating a correctly-sized buffer
each time through the loop. This increases the runtime by as much as 219%.

Column 2 is a naive use of strbuf, where each time through the loop the
strbuf is reinitialized to STRBUF_INIT, and managing the space is
entirely left to strbuf.

Column 3 is like column 2, except that it initializes the strbuf to the
correct size right away using strbuf_init(). This reduces the runtime
relative to column 2 by as much as 35%.

Column 4 uses a simulated "fixed strbuf", where the fixed-size buffer is
big enough for the full string (thus there are no calls to
malloc()/realloc()/free()).

The comparison between columns 0 and 4 shows that using a strbuf costs
as much as 123% more than using a simple char array, even if the strbuf
doesn't have to do any memory allocations.

The comparison between columns 3 and 4 shows savings a reduction in
runtime of up to 55% from using a "fixed strbuf" rather than a pre-sized
conventional strbuf. I think this is the comparison that is most
relevant to the current discussion.

Of course strbuf manipulation (especially of small numbers of strings)
is unlikely to be a big fraction of the workload of any Git command, so
this is far from proof that this optimization is worthwhile in terms of
code complexity. But I am still moderately in favor of the idea, for two
reasons:

1. The amount of added code complexity is small and quite
   encapsulated.

2. The ability to use strbufs without having to allocate memory might
   make enough *psychological* difference that it encourages some
   devs to use strbufs where they would otherwise have done manual
   memory management. I think this would be a *big* win in terms of
   potential bugs and security vulnerabilities avoided.

Michael

  reply	other threads:[~2016-06-02 11:12 UTC|newest]

Thread overview: 38+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2016-05-30 10:36 [PATCH 0/2] strbuf: improve API William Duclot
2016-05-30 10:36 ` [PATCH 1/2] strbuf: add tests William Duclot
2016-05-30 11:26   ` Johannes Schindelin
2016-05-30 13:42     ` Simon Rabourg
2016-05-30 11:56   ` Matthieu Moy
2016-05-31  2:04   ` Michael Haggerty
2016-05-31  9:48     ` Simon Rabourg
2016-05-30 10:36 ` [PATCH 2/2] strbuf: allow to use preallocated memory William Duclot
2016-05-30 12:13   ` Johannes Schindelin
2016-05-30 13:20     ` William Duclot
2016-05-31  6:21       ` Johannes Schindelin
2016-05-31  3:05     ` Michael Haggerty
2016-05-31  6:41       ` Johannes Schindelin
2016-05-31  8:25         ` Michael Haggerty
2016-05-30 12:52   ` Matthieu Moy
2016-05-30 14:15     ` William Duclot
2016-05-30 14:34       ` Matthieu Moy
2016-05-30 15:16         ` William Duclot
2016-05-31  4:05     ` Michael Haggerty
2016-05-31 15:59       ` William Duclot
2016-06-03 14:04       ` William Duclot
2016-05-30 21:56   ` Mike Hommey
2016-05-30 22:46     ` William Duclot
2016-05-30 22:50       ` Mike Hommey
2016-05-31  6:34   ` Junio C Hamano
2016-05-31 15:45     ` William
2016-05-31 15:54       ` Matthieu Moy
2016-05-31 16:08         ` William Duclot
2016-05-30 11:32 ` [PATCH 0/2] strbuf: improve API Remi Galan Alfonso
2016-06-01  7:42   ` Jeff King
2016-06-01 19:50     ` David Turner
2016-06-01 20:09       ` Jeff King
2016-06-01 20:22         ` David Turner
2016-06-01 21:07     ` Jeff King
2016-06-02 11:11       ` Michael Haggerty [this message]
2016-06-02 12:58         ` Matthieu Moy
2016-06-02 14:22           ` William Duclot
2016-06-24 17:20         ` Jeff King

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=5750147C.5060609@alum.mit.edu \
    --to=mhagger@alum.mit.edu \
    --cc=antoine.queru@ensimag.grenoble-inp.fr \
    --cc=francois.beutin@ensimag.grenoble-inp.fr \
    --cc=git@vger.kernel.org \
    --cc=matthieu.moy@grenoble-inp.fr \
    --cc=peff@peff.net \
    --cc=remi.galan-alfonso@ensimag.grenoble-inp.fr \
    --cc=simon.rabourg@ensimag.grenoble-inp.fr \
    --cc=william.duclot@ensimag.grenoble-inp.fr \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.