Skip to contentSkip to conversation

Big git diff speedup by avoiding x86 "fast string" memcmp

PATCH Nick Piggin 9 participants 25 messages Dec 9, 2010 – Dec 16, 2010

Subsystems (2)
lore
Share & tools
lore
#0 lore raw

I was actually discussing this with Linus a while back, and finally got around to testing it out now that I have a modern CPU to measure it on! CCing linux-arch because it would be interesting to know whether your tuned functions do better than gcc or not (I would suspect not).

BTW. patch and numbers are on top of my scaling series, just for an idea of what it does, I just want to generate some interesting discussion.

If people are interested in running benchmarks, I'll be pushing out a new update soon, after some more testing and debugging here.

The standard memcmp function on a Westmere system shows up hot in profiles in the git diff workload (both parallel and single threaded), and it is likely due to the costs associated with trapping into microcode, and little opportunity to improve memory access (dentry name is not likely to take up more than a cacheline).

So replace it with an open-coded byte comparison. This increases code size by 24 bytes in the critical __d_lookup_rcu function, but the speedup is huge, averaging 10 runs of each:

git diff st   user   sys   elapsed  CPU
before        1.15   2.57  3.82      97.1
after         1.14   2.35  3.61      96.8
git diff mt   user   sys   elapsed  CPU
before        1.27   3.85  1.46     349
after         1.26   3.54  1.43     333

Elapsed time for single threaded git diff at 95.0% confidence:

        -0.21  +/- 0.01
        -5.45% +/- 0.24%

Parallel case doesn't gain much, but that's because userspace runs out of work to feed it -- efficiency is way up, though.

fs/dcache.c +3 -3
Index: linux-2.6/fs/dcache.c
===================================================================
@@ -1423,7 +1423,7 @@ static struct dentry *__d_instantiate_un
 			goto next;
 		if (qstr->len != len)
 			goto next;
-		if (memcmp(qstr->name, name, len))
+		if (dentry_memcmp(qstr->name, name, len))
 			goto next;
 		__dget_dlock(alias);
 		spin_unlock(&alias->d_lock);
@@ -1771,7 +1771,7 @@ struct dentry *__d_lookup_rcu(struct den
 		} else {
 			if (tlen != len)
 				continue;
-			if (memcmp(tname, str, tlen))
+			if (dentry_memcmp(tname, str, tlen))
 				continue;
 		}
 		/*
@@ -1901,7 +1901,7 @@ struct dentry *__d_lookup(struct dentry
 		} else {
 			if (tlen != len)
 				goto next;
-			if (memcmp(tname, str, tlen))
+			if (dentry_memcmp(tname, str, tlen))
 				goto next;
 		}
 
include/linux/dcache.h +14 -0
Index: linux-2.6/include/linux/dcache.h
===================================================================
@@ -47,6 +47,20 @@ struct dentry_stat_t {
 };
 extern struct dentry_stat_t dentry_stat;
 
+static inline int dentry_memcmp(const unsigned char *cs,
+				const unsigned char *ct, size_t count)
+{
+	while (count) {
+		int ret = (*cs != *ct);
+		if (ret)
+			return ret;
+		cs++;
+		ct++;
+		count--;
+	}
+	return 0;
+}
+
 /* Name hashing routines. Initial hash value */
 /* Hash courtesy of the R5 hash in reiserfs modulo sign bits */
 #define init_name_hash()		0
#1 lore raw
Quoted 27 lines · Nick Piggin

I was actually discussing this with Linus a while back, and finally got around to testing it out now that I have a modern CPU to measure it on! CCing linux-arch because it would be interesting to know whether your tuned functions do better than gcc or not (I would suspect not).

BTW. patch and numbers are on top of my scaling series, just for an idea of what it does, I just want to generate some interesting discussion.

If people are interested in running benchmarks, I'll be pushing out a new update soon, after some more testing and debugging here.

The standard memcmp function on a Westmere system shows up hot in profiles in the git diff workload (both parallel and single threaded), and it is likely due to the costs associated with trapping into microcode, and little opportunity to improve memory access (dentry name is not likely to take up more than a cacheline).

So replace it with an open-coded byte comparison. This increases code size by 24 bytes in the critical __d_lookup_rcu function, but the speedup is huge, averaging 10 runs of each:

git diff st   user   sys   elapsed  CPU
before        1.15   2.57  3.82      97.1
after         1.14   2.35  3.61      96.8
Quoted 7 lines
git diff mt   user   sys   elapsed  CPU
before        1.27   3.85  1.46     349
after         1.26   3.54  1.43     333

Elapsed time for single threaded git diff at 95.0% confidence:

        -0.21  +/- 0.01
        -5.45% +/- 0.24%

Nice.

[..]

include/linux/dcache.h struct dentry_stat_t { · line 62

Quoted 13 lines
include/linux/dcache.h +13 -0
+static inline int dentry_memcmp(const unsigned char *cs,
+				const unsigned char *ct, size_t count)
+{
+	while (count) {
+		int ret = (*cs != *ct);
+		if (ret)
+			return ret;
+		cs++;
+		ct++;
+		count--;
+	}
+	return 0;
+}

we have a memcmp() in lib/string.c. Maybe reuse it from there?

Signature · 2 lines
Regards/Gruss,
Boris.
#2 lore raw
Quoted 16 lines · Borislav Petkov

include/linux/dcache.h struct dentry_stat_t { · line 62

Quoted 13 lines · Nick Piggin
include/linux/dcache.h +13 -0
+static inline int dentry_memcmp(const unsigned char *cs,
+				const unsigned char *ct, size_t count)
+{
+	while (count) {
+		int ret = (*cs != *ct);
+		if (ret)
+			return ret;
+		cs++;
+		ct++;
+		count--;
+	}
+	return 0;
+}

we have a memcmp() in lib/string.c. Maybe reuse it from there?

Well I think I prefer it to be inline so it doesn't clobber registers, and also so that it isn't required to carry an integer value to return (only boolean) if that might help.

I might also change it a bit and take advantage of the fact we won't have a zero sized count -- this could move the loop branch to after the values are loaded, and would mean that the load will get underway even if the branch is mispredicted...

#3 lore raw
In reply to Borislav Petkov
Quoted 16 lines · Borislav Petkov

include/linux/dcache.h struct dentry_stat_t { · line 62

Quoted 13 lines · Nick Piggin
include/linux/dcache.h +13 -0
+static inline int dentry_memcmp(const unsigned char *cs,
+				const unsigned char *ct, size_t count)
+{
+	while (count) {
+		int ret = (*cs != *ct);
+		if (ret)
+			return ret;
+		cs++;
+		ct++;
+		count--;
+	}
+	return 0;
+}

we have a memcmp() in lib/string.c. Maybe reuse it from there?

Well I think I prefer it to be inline so it doesn't clobber registers, and also so that it isn't required to carry an integer value to return (only boolean) if that might help.

I might also change it a bit and take advantage of the fact we won't have a zero sized count -- this could move the loop branch to after the values are loaded, and would mean that the load will get underway even if the branch is mispredicted...

#4 lore raw
In reply to Nick Piggin
Quoted 2 lines · Nick Piggin

So replace it with an open-coded byte comparison. This increases code size by 24 bytes in the critical __d_lookup_rcu function, but the

Actually, if the loop assumes len is non zero (which is the case for dentry compare), then the bloat is only 8 bytes, so not a problem.

Also got numbers versus vanilla kernel, out of interest.

Quoted 3 lines

speedup is huge, averaging 10 runs of each:

git diff st user sys elapsed CPU

  vanilla       1.19   3.21  4.47      98.0
> before        1.15   2.57  3.82      97.1
> after         1.14   2.35  3.61      96.8
Quoted 2 lines

git diff mt user sys elapsed CPU

  vanilla       1.57  45.75  3.60    1312
> before        1.27   3.85  1.46     349
> after         1.26   3.54  1.43     333
Quoted 1 line

Single thread elapsed time improvment vanilla vs vfs 19.23%. Not quite as big as the AMD fam10h speedup, that's probably because Westmere does atomics so damn quickly.

Multi thread numbers are no surprise.

#5 lore raw
In reply to Nick Piggin

Nick Piggin:

Quoted 5 lines

The standard memcmp function on a Westmere system shows up hot in profiles in the git diff workload (both parallel and single threaded), and it is likely due to the costs associated with trapping into microcode, and little opportunity to improve memory access (dentry name is not likely to take up more than a cacheline).

Let me make sure.
What you are pointing out is
- asm("repe; cmpsb") may grab CPU long time, and can be a hazard for scaling.
- by breaking it into pieces, the chances to scale will increase. Right?

Anyway this appraoch replacing smallest code by larger but faster code is interesting.
How about mixing 'unsigned char *' and 'unsigned long *' in referencing the given strings?
For example,

int f(const unsigned char *cs, const unsigned char *ct, size_t count)

{
	int ret;
	union {
		const unsigned long *l;
		const unsigned char *c;
	} s, t;

/* this macro is your dentry_memcmp() actually */

#define cmp(s, t, c, step)		      \
	do {				      \
		while ((c) >= (step)) {	      \
			ret = (*(s) != *(t)); \
			if (ret)	      \
				return ret;   \
			(s)++;		      \
			(t)++;		      \
			(c) -= (step);	      \
		}			      \
	} while (0)

	s.c = cs;
	t.c = ct;
	cmp(s.l, t.l, count, sizeof(*s.l));
	cmp(s.c, t.c, count, sizeof(*s.c));
	return 0;
}

What I am thinking here is,
- in load and compare, there is no difference between 'char*' and 'long*', probably.
- obviously 'step by sizeof(long)' will reduce the number of repeats.
- but I am not sure whether the length of string is generally longer than 4 (or 8) or not.

J. R. Okajima

#6 lore raw
Quoted 8 lines · J. R. Okajima

Nick Piggin:

Quoted 5 lines

The standard memcmp function on a Westmere system shows up hot in profiles in the git diff workload (both parallel and single threaded), and it is likely due to the costs associated with trapping into microcode, and little opportunity to improve memory access (dentry name is not likely to take up more than a cacheline).

Quoted 6 lines

Let me make sure.
What you are pointing out is
- asm("repe; cmpsb") may grab CPU long time, and can be a hazard for scaling.
- by breaking it into pieces, the chances to scale will increase. Right?

It's not scaling but just single threaded performance. gcc turns memcmp into rep cmp, which has quite a long latency, so it's not appripriate for short strings.

Quoted 34 lines

Anyway this appraoch replacing smallest code by larger but faster code is interesting.
How about mixing 'unsigned char *' and 'unsigned long *' in referencing the given strings?
For example,

int f(const unsigned char *cs, const unsigned char *ct, size_t count)

{
	int ret;
	union {
		const unsigned long *l;
		const unsigned char *c;
	} s, t;

/* this macro is your dentry_memcmp() actually */

#define cmp(s, t, c, step)		      \
	do {				      \
		while ((c) >= (step)) {	      \
			ret = (*(s) != *(t)); \
			if (ret)	      \
				return ret;   \
			(s)++;		      \
			(t)++;		      \
			(c) -= (step);	      \
		}			      \
	} while (0)

	s.c = cs;
	t.c = ct;
	cmp(s.l, t.l, count, sizeof(*s.l));
	cmp(s.c, t.c, count, sizeof(*s.c));
	return 0;
}
Quoted 6 lines

What I am thinking here is,
- in load and compare, there is no difference between 'char*' and 'long*', probably.
- obviously 'step by sizeof(long)' will reduce the number of repeats.
- but I am not sure whether the length of string is generally longer than 4 (or 8) or not.

The comparison is no longer an issue, so I think the added complexity is not going to be worth it -- think about average length of directory entry name, the average is maybe 12? In the kernel tree it's 11.

If we really wanted to do this, we'd round name lengths up to nearest sizeof(long) (which should be the case already, but we'd do it explicitly), zero fill the last bytes, and do a long compare loop. I doubt it would be noticable though.

#7 lore raw

Nick Piggin:

Quoted 3 lines

It's not scaling but just single threaded performance. gcc turns memcmp into rep cmp, which has quite a long latency, so it's not appripriate for short strings.

Honestly speaking I doubt how this 'long *' approach is effective (Of course it never means that your result (by 'char *') is doubtful). But is the "rep cmp has quite a long latency" issue generic for all x86 architecture, or Westmere system specific?

J. R. Okajima

#8 lore raw
Quoted 8 lines · J. R. Okajima

Nick Piggin:

Quoted 3 lines

It's not scaling but just single threaded performance. gcc turns memcmp into rep cmp, which has quite a long latency, so it's not appripriate for short strings.

Honestly speaking I doubt how this 'long *' approach is effective (Of course it never means that your result (by 'char *') is doubtful).

Well, let's see what turns up. We certainly can try the long * approach. I suspect on architectures where byte loads are very slow, gcc will block the loop into larger loads, so it should be no worse than a normal memcmp call, but if we do explicit padding we can avoid all the problems associated with tail handling.

Doing name padding and long * comparison will be practically free (because slab allocator will align out to sizeof(long long) anyway), so if any architecture prefers to do the long loads, I'd be interested to hear and we could whip up a patch.

Quoted 2 lines

But is the "rep cmp has quite a long latency" issue generic for all x86 architecture, or Westmere system specific?

I don't believe it is Westmere specific. Intel and AMD have been improving these instructions in the past few years, so Westmere is probably as good or better than any.

That said, rep cmp may not be as heavily optimized as the set and copy string instructions.

In short, I think the change should be suitable for all x86 CPUs, but I would like to hear more opinions or see numbers for other cores.

Thanks,
Nick

#9 lore raw

Nick Piggin:

Quoted 6 lines

Well, let's see what turns up. We certainly can try the long * approach. I suspect on architectures where byte loads are very slow, gcc will block the loop into larger loads, so it should be no worse than a normal memcmp call, but if we do explicit padding we can avoid all the problems associated with tail handling.

Thank you for your reply.
But unfortunately I am afraid that I cannot understand what you wrote clearly due to my poor English. What I understood is,
- I suggested 'long *' approach
- You wrote "not bad and possible, but may not be worth"
- I agreed "the approach may not be effective"
And you gave deeper consideration, but the result is unchaged which means "'long *' approach may not be worth". Am I right?

Quoted 3 lines

In short, I think the change should be suitable for all x86 CPUs, but I would like to hear more opinions or see numbers for other cores.

I'd like to hear from other x86 experts too.
Also I noticed that memcmp for x86_32 is defined as __builtin_memcmp (for x86_64 is "rep cmp"). Why does x86_64 doesn't use __builtin_memcmp? Is it really worse?

J. R. Okajima

#10 lore raw
Quoted 9 lines · J. R. Okajima

Nick Piggin:

Quoted 6 lines

Well, let's see what turns up. We certainly can try the long * approach. I suspect on architectures where byte loads are very slow, gcc will block the loop into larger loads, so it should be no worse than a normal memcmp call, but if we do explicit padding we can avoid all the problems associated with tail handling.

Quoted 8 lines

Thank you for your reply.
But unfortunately I am afraid that I cannot understand what you wrote clearly due to my poor English. What I understood is,
- I suggested 'long *' approach
- You wrote "not bad and possible, but may not be worth"
- I agreed "the approach may not be effective"
And you gave deeper consideration, but the result is unchaged which means "'long *' approach may not be worth". Am I right?

What I meant is that a "normal" memcmp that does long * memory operations is not a good idea, because it requires code and branches to handle the tail of the string. When average string lengths are less than 16 bytes, it hardly seems wothwhile. It will just get more mispredicts and bigger icache footprint.

However instead of a normal memcmp, we could actually pad dentry names out to sizeof(long) with zeros, and take advantage of that with a memcmp that does not have to handle tails -- it would operate entirely with longs.

That would avoid icache and branch regressions, and might speed up the operation on some architectures. I just doubted whether it would show an improvement to be worth doing at all. If it does, I'm all for it.

Quoted 12 lines
Quoted 3 lines

In short, I think the change should be suitable for all x86 CPUs, but I would like to hear more opinions or see numbers for other cores.

I'd like to hear from other x86 experts too.
Also I noticed that memcmp for x86_32 is defined as __builtin_memcmp (for x86_64 is "rep cmp"). Why does x86_64 doesn't use __builtin_memcmp? Is it really worse?

J. R. Okajima

#11 lore raw

Thank you for explanation.
I understood about padding zero tail bytes as an improvement or a variation of the 'long *' approach. But I didn't write about it in my previous mail. If I had written, you might not need to describe again.

I have also compared "repe; cmpsb" and __builtin_memcmp "in userspace". And __builtin_memcmp shows faster and it looks like an approach of mixing pointers. Although the code size grows, in source it will be simple #define and it is already done in x86_32. Do you (or any other x86 experts) think it is worth to try?

J. R. Okajima

Nick Piggin:

Quoted 14 lines

What I meant is that a "normal" memcmp that does long * memory operations is not a good idea, because it requires code and branches to handle the tail of the string. When average string lengths are less than 16 bytes, it hardly seems wothwhile. It will just get more mispredicts and bigger icache footprint.

However instead of a normal memcmp, we could actually pad dentry names out to sizeof(long) with zeros, and take advantage of that with a memcmp that does not have to handle tails -- it would operate entirely with longs.

That would avoid icache and branch regressions, and might speed up the operation on some architectures. I just doubted whether it would show an improvement to be worth doing at all. If it does, I'm all for it.

#12 lore raw
In reply to Nick Piggin
Quoted 10 lines · Nick Piggin

However instead of a normal memcmp, we could actually pad dentry names out to sizeof(long) with zeros, and take advantage of that with a memcmp that does not have to handle tails -- it would operate entirely with longs.

That would avoid icache and branch regressions, and might speed up the operation on some architectures. I just doubted whether it would show an improvement to be worth doing at all. If it does, I'm all for it.

I agree that the byte-compare or long-compare should give you very close results in modern pipeline CPUs. But surly 12 increments-and-test should show up against 3 (or even 2). I would say it must be a better plan.

BTW the long approach if you assume that the beginning of the string is long aligned than it is only a matter of comparing the last byte with a mask, no branches. But I'm not saying, just make sure they are padded.

Just my $0.017
Thanks
Boaz

#13 lore raw

From: Boaz Harrosh <[email protected]>
Date: Wed, 15 Dec 2010 15:15:09 +0200

Quoted 3 lines

I agree that the byte-compare or long-compare should give you very close results in modern pipeline CPUs. But surly 12 increments-and-test should show up against 3 (or even 2). I would say it must be a better plan.

For strings of these lengths the setup code necessary to initialize the inner loop and the tail code to handle the sub-word ending cases eliminate whatever gains there are.

I know this as I've been hacking on assembler optimized strcmp() and memcmp() in my spare time over the past year or so.

#14 lore raw
Quoted 11 lines · David Miller

From: Boaz Harrosh <[email protected]>
Date: Wed, 15 Dec 2010 15:15:09 +0200

Quoted 3 lines

I agree that the byte-compare or long-compare should give you very close results in modern pipeline CPUs. But surly 12 increments-and-test should show up against 3 (or even 2). I would say it must be a better plan.

For strings of these lengths the setup code necessary to initialize the inner loop and the tail code to handle the sub-word ending cases eliminate whatever gains there are.

You miss understood me. I'm saying that we know the beggining of the string is aligned and Nick offered to pad the last long, so surly a shift by 2 (or 3) + the reduction of the 12 dec-and-test to 3 should give you an optimization?

Quoted 2 lines

I know this as I've been hacking on assembler optimized strcmp() and memcmp() in my spare time over the past year or so.

Boaz

#15 lore raw
Quoted 13 lines · Boaz Harrosh
Quoted 11 lines · David Miller

From: Boaz Harrosh <[email protected]>
Date: Wed, 15 Dec 2010 15:15:09 +0200

Quoted 3 lines

I agree that the byte-compare or long-compare should give you very close results in modern pipeline CPUs. But surly 12 increments-and-test should show up against 3 (or even 2). I would say it must be a better plan.

For strings of these lengths the setup code necessary to initialize the inner loop and the tail code to handle the sub-word ending cases eliminate whatever gains there are.

Quoted 4 lines

You miss understood me. I'm saying that we know the beggining of the string is aligned and Nick offered to pad the last long, so surly a shift by 2 (or 3) + the reduction of the 12 dec-and-test to 3 should give you an optimization?

Masking is still going to take a bit more code, but perhaps. We're talking a handful of cycles here, so if you add a branch mispredict, or icache miss, you'll kill your savings.

This is what I've got at the moment, which adds only 8 bytes over the rep cmp variant, in the __d_lookup_rcu function.

static inline int dentry_memcmp(const unsigned char *cs,
const unsigned char *ct, size_t count)

{
       int ret;
       do {
               ret = (*cs != *ct);
               if (ret)
                       break;
               cs++;
               ct++;
               count--;
       } while (count);
       return ret;
}

Now if we pad and zero fill the dentry name, then we can compare with the path string, but we still have to mask that guy (unfortunately, I didn't consider that earlier) so it's not trivial and adds quite a bit of code size and branches:

static inline int dentry_memcmp_long(const unsigned char *cs,
const unsigned char *ct, ssize_t count)

{
        const unsigned long *ls = (const unsigned long *)cs;
        const unsigned long *lt = (const unsigned long *)ct;

       int ret;
       do {
                unsigned long t = *lt;
                unsigned long s = *ls;
                int shift = 0;

                if (count < 8)
                        shift = (8 - count) * 8;
                t = t & (0xffffffffffffffff >> shift);
               ret = (s != t);
               if (ret)
                       break;
               ls++;
               lt++;
               count-=8;
       } while (count > 0);
       return ret;
}

Something like this should work on little endian. You'd have to coax gcc to generate a cmov to get rid of that branch I think, because it won't be predictable for small string lengths. But then you have to think about icache...

#16 lore raw
Quoted 71 lines · Nick Piggin
Quoted 13 lines · Boaz Harrosh
Quoted 11 lines · David Miller

From: Boaz Harrosh <[email protected]>
Date: Wed, 15 Dec 2010 15:15:09 +0200

Quoted 3 lines

I agree that the byte-compare or long-compare should give you very close results in modern pipeline CPUs. But surly 12 increments-and-test should show up against 3 (or even 2). I would say it must be a better plan.

For strings of these lengths the setup code necessary to initialize the inner loop and the tail code to handle the sub-word ending cases eliminate whatever gains there are.

Quoted 4 lines

You miss understood me. I'm saying that we know the beggining of the string is aligned and Nick offered to pad the last long, so surly a shift by 2 (or 3) + the reduction of the 12 dec-and-test to 3 should give you an optimization?

Masking is still going to take a bit more code, but perhaps. We're talking a handful of cycles here, so if you add a branch mispredict, or icache miss, you'll kill your savings.

This is what I've got at the moment, which adds only 8 bytes over the rep cmp variant, in the __d_lookup_rcu function.

static inline int dentry_memcmp(const unsigned char *cs,
const unsigned char *ct, size_t count)

{
       int ret;
       do {
               ret = (*cs != *ct);
               if (ret)
                       break;
               cs++;
               ct++;
               count--;
       } while (count);
       return ret;
}

Now if we pad and zero fill the dentry name, then we can compare with the path string, but we still have to mask that guy (unfortunately, I didn't consider that earlier) so it's not trivial and adds quite a bit of code size and branches:

static inline int dentry_memcmp_long(const unsigned char *cs,
const unsigned char *ct, ssize_t count)

{
        const unsigned long *ls = (const unsigned long *)cs;
        const unsigned long *lt = (const unsigned long *)ct;

       int ret;
       do {
                unsigned long t = *lt;
                unsigned long s = *ls;
                int shift = 0;

                if (count < 8)
                        shift = (8 - count) * 8;
                t = t & (0xffffffffffffffff >> shift);
               ret = (s != t);
               if (ret)
                       break;
               ls++;
               lt++;
               count-=8;
       } while (count > 0);
       return ret;
}
Quoted 4 lines

Something like this should work on little endian. You'd have to coax gcc to generate a cmov to get rid of that branch I think, because it won't be predictable for small string lengths. But then you have to think about icache...

static inline int dentry_memcmp_long(const unsigned char *cs,
const unsigned char *ct, ssize_t count)

{
        int ret;
	const unsigned long *ls = (const unsigned long *)cs;
	const unsigned long *lt = (const unsigned long *)ct;

        while (count > 8) {
                ret = (*cs != *ct);
                if (ret)
                        break;
                cs++;
                ct++;
                count-=8;
        }
	if (count) {
		unsigned long t = *ct & ((0xffffffffffffffff >> ((8 - count) * 8))
		ret = (*cs != t)
	}

        return ret;
}

Same as yours but just to avoid the branch inside the loop and slightly smaller code.

BTW: On some ARCHs ++foo is faster than foo++

     and Also, is there a reason not to write the above loop as:

     while(c-- && (ret = (*cs++ != *ct++)))
		;

Thanks
Boaz

#17 lore raw
Quoted 21 lines · Boaz Harrosh

On 12/16/2010 03:13 PM, Nick Piggin wrote:
static inline int dentry_memcmp_long(const unsigned char *cs, const unsigned char *ct, ssize_t count) {
       int ret;
       const unsigned long *ls = (const unsigned long *)cs; const unsigned long *lt = (const unsigned long *)ct;

       while (count > 8) {
               ret = (*cs != *ct);
               if (ret)
                       break;
               cs++;
               ct++;
               count-=8;
       }
       if (count) {
               unsigned long t = *ct & ((0xffffffffffffffff >> ((8 - count) * 8)) ret = (*cs != t)
       }

Quoted 5 lines

       return ret;
}

Same as yours but just to avoid the branch inside the loop and slightly smaller code.

That's true, it should make the branch more predictable too. Well, maybe. I'm going to leave it as-is for now, but I would welcome any test results or ideas for improvements.

Quoted 5 lines

BTW: On some ARCHs ++foo is faster than foo++
    and Also, is there a reason not to write the above loop as:

    while(c-- && (ret = (*cs++ != *ct++)))
               ;

gcc should turn it into the same thing these days. I prefer avoiding expressions with side effects in control statements (other than a simple counter).

Thanks,
Nick

#18 lore raw
In reply to Boaz Harrosh
Quoted 5 lines · Boaz Harrosh

You miss understood me. I'm saying that we know the beggining of the string is aligned and Nick offered to pad the last long, so surly a shift by 2 (or 3) + the reduction of the 12 dec-and-test to 3 should give you an optimization?

Sadly, right now we don't know that the string is necessarily even aligned.

Yes, it's always aligned in a dentry, because it's either the inline short string, or it's the longer string we explicitly allocated to the dentry.

But when we do name compares in __d_lookup, only one part of that is a dentry. The other is a qstr, and the name there is not aligned. In fact, it's not even NUL-terminated. It's the data directly from the path itself.

So we can certainly do compares a "long" at a time, but it's not entirely trivial. And just making the dentries be aligned and null-padded is not enough. Most likely, you'd have to make the dentry name compare function do an unaligned load from the qstr part, and then do the masking.

Which is likely still the best performance on something like x86 where unaligned loads are cheap, but on other architectures it might be less so.

Linus

#19 lore raw
In reply to Boaz Harrosh

From: Boaz Harrosh <[email protected]>
Date: Thu, 16 Dec 2010 11:53:09 +0200

Quoted 4 lines

You miss understood me. I'm saying that we know the beggining of the string is aligned and Nick offered to pad the last long, so surly a shift by 2 (or 3) + the reduction of the 12 dec-and-test to 3 should give you an optimization?

Indeed, we could do something interesting in the case where both strings are tail padded like that.

#20 lore raw
In reply to Nick Piggin
Quoted 8 lines · Nick Piggin
Quoted 2 lines · J. R. Okajima

But is the "rep cmp has quite a long latency" issue generic for all x86 architecture, or Westmere system specific?

I don't believe it is Westmere specific. Intel and AMD have been improving these instructions in the past few years, so Westmere is probably as good or better than any.

Quoted 7 lines

That said, rep cmp may not be as heavily optimized as the set and copy string instructions.

In short, I think the change should be suitable for all x86 CPUs, but I would like to hear more opinions or see numbers for other cores.

How about other arch? If this is only for x86, shouldn't it be protected by CONFIG_X86?

Thanks.

#21 lore raw
Quoted 12 lines · Américo Wang
Quoted 11 lines · Nick Piggin
Quoted 2 lines · J. R. Okajima

But is the "rep cmp has quite a long latency" issue generic for all x86 architecture, or Westmere system specific?

I don't believe it is Westmere specific. Intel and AMD have been improving these instructions in the past few years, so Westmere is probably as good or better than any.

That said, rep cmp may not be as heavily optimized as the set and copy string instructions.

Quoted 7 lines
Quoted 4 lines

In short, I think the change should be suitable for all x86 CPUs, but I would like to hear more opinions or see numbers for other cores.

How about other arch? If this is only for x86, shouldn't it be protected by CONFIG_X86?

That's what I would like to know, but I suspect that for very short strings we are dealing with, the custom loop will be fine for everybody.

#22 lore raw
Quoted 4 lines · Nick Piggin

That's what I would like to know, but I suspect that for very short strings we are dealing with, the custom loop will be fine for everybody.

Yeah, I can pretty much guarantee it for the common case of short strings. Most path components are short enough that a "clever" memcmp() is simply likely going to be slower than doing things byte-per-byte.

That's especially true if we then in the future end up making it do a long-by-long compare instead of the byte-by-byte one.

Linus

#23 lore raw
In reply to Nick Piggin

include/linux/dcache.h struct dentry_stat_t { · line 62

Quoted 13 lines · Nick Piggin
include/linux/dcache.h +13 -0
+static inline int dentry_memcmp(const unsigned char *cs,
+                               const unsigned char *ct, size_t count)
+{
+       while (count) {
+               int ret = (*cs != *ct);
+               if (ret)
+                       return ret;
+               cs++;
+               ct++;
+               count--;
+       }
+       return 0;
+}

Since you are proposing a routine that only compares file names - I wonder whether it would be faster to start at the end and work backwards? If the filenames are the same, it makes no difference - you have to look at all the bytes. But if they are different you might bail out earlier. There are many applications that stick a common prefix onto the start of filenames (just look in "/lib" !), but I think it is less common to add a suffix (longer than "." single letter).

-Tony

#24 lore raw
Quoted 15 lines · Tony Luck

include/linux/dcache.h struct dentry_stat_t { · line 62

Quoted 13 lines · Nick Piggin
include/linux/dcache.h +13 -0
+static inline int dentry_memcmp(const unsigned char *cs,
+                               const unsigned char *ct, size_t count)
+{
+       while (count) {
+               int ret = (*cs != *ct);
+               if (ret)
+                       return ret;
+               cs++;
+               ct++;
+               count--;
+       }
+       return 0;
+}
Quoted 8 lines

Since you are proposing a routine that only compares file names - I wonder whether it would be faster to start at the end and work backwards? If the filenames are the same, it makes no difference - you have to look at all the bytes. But if they are different you might bail out earlier. There are many applications that stick a common prefix onto the start of filenames (just look in "/lib" !), but I think it is less common to add a suffix (longer than "." single letter).

That's true, and an interesting point. However I have managed to fit the first 8 bytes of the name (in the case of shortnames) into the same single dentry cacheline that is used for path walking. So that might negate some of the benefits of walking backwards.

I would encourage anybody to grab the branch and try out any tweaks, though...