generic rwsem [Re: Alpha "process table hang"]
Quoted 1 line
It is 36bytes. and on 64bit archs the difference is going to be less.
You're right - I can't add up (must be too late at night), and I was looking at wait_queue not wait_queue_head. I suppose that means my implementations are then 20 and 16 bytes respectively.
On 64-bit archs the difference will be less, depending on what a "long" is.
Quoted 3 lines
The real waste is the lock of the waitqueue that I don't need, so I should probably keep two
list_headin the waitqueue instead of using thewait_queue_head_tandwake_up_processby hand.
Perhaps you should steal my wake_up_ctx() idea. That means you only need one wait queue, and you use bits in the wait_queue flags to note which type of waiter is at the front of the queue.
You can then say "wake up the first thing at the front of the queue if it is a writer"; and you can say "wake up the first consequtive bunch of things at the front of the queue, provided they're all readers" or "wake up all the readers in the queue".
Quoted 3 lines
The fast path has to be as fast as yours, if not then the only variable that can make difference is the fact I'm not inlining the fast path because it's not that small, in such a case I should simply inline the fast path
My point exactly... It can't be as fast because it's _all_ out of line. Therefore you always have to go through the overhead of a function call, whatever that entails on the architecture of choice.
Quoted 4 lines
I don't care about the scalability of the slow path and I think the slow path may even be faster than yours because I don't run additional unlock/lock and memory barriers and the other cpus will stop dirtifying my stuff after their first trylock until I unlock.
Except for the optimised case, you may be correct on an SMP configured kernel (for a UP kernel, spinlocks are nops).
However! mine runs for as little time as possible with spinlocks held in the generic case, and, perhaps more importantly, as little time as possible with interrupts disabled.
One other thing: should you be using spin_lock_irqsave() instead of spin_lock_irq() in your down functions? I'm not sure it's necessary, however, since you probably shouldn't be sleeping if you've got the interrupts disabled (though schedule() will cope).
Quoted 3 lines
If you have time to benchmark I'd be interested to see some number. But anyways my implementation was mostly meant to be obviously right and possible to ovverride with per-arch algorithms
I'll have a go tomorrow evening. It's time to go to bed now I think:-)
David
Quoted 7 lines · D . W . Howells
Quoted 1 line
It is 36bytes. and on 64bit archs the difference is going to be less.
You're right - I can't add up (must be too late at night), and I was looking at
wait_queuenotwait_queue_head. I suppose that means my implementations are then 20 and 16 bytes respectively.On 64-bit archs the difference will be less, depending on what a "long" is.
yes. I actually modified my implementation and now I gone below the size of a waitqueue because I don't need its lock. The rw_semaphore now is only 16 bytes in size even in SMP (while your generic rw_semaphore is larger than 16 in smp). I also changed the wakeup mechanism to do the same fair logic as yours but I'm not changing any common code (so it's complete FIFO with a wake up all contigous readers behaviour). It's also now completly fair as the fast path will go to sleep if anybody is registered in a waitqueue so it has the property that we are still missing in the non rw semaphores. And it still seems quite obvious while reading it.
Quoted 1 line
Perhaps you should steal my
wake_up_ctx()idea. That means you only need one
I now stolen the wakeup logic but I reimplemented it internally to the rwsem.c without involving the external visible waitqueue mechanism (short version: no changes to sched.c and wait.h).
Quoted 4 lines
You can then say "wake up the first thing at the front of the queue if it is a writer"; and you can say "wake up the first consequtive bunch of things at the front of the queue, provided they're all readers" or "wake up all the readers in the queue".
I preferred not to generalize that in the wake_up_ctx way but yes I hardcoded that in a function that knows what to do with a simple list_head that is even ligther and faster.
Quoted 1 line
My point exactly... It can't be as fast because it's _all_ out of line.
Ok, I also inlined the fast path. Of course if you're not running out of icache (like in a benchmark dedicated to the rwsem) inlining the fast path is faster... (previously I was talking about real world misc load where we can more easily run out of icache)
Quoted 3 lines
However! mine runs for as little time as possible with spinlocks held in the generic case, and, perhaps more importantly, as little time as possible with interrupts disabled.
But it reacquires other locks for the waitqueue and it clears irqs again too and it will ping pong cachelines in the wait event interface. So it's _slower_ and not faster. Making the locks more granular makes sense when the contention on the lock goes away after you make it granular, but you are using two spinlocks instead of one and you still have contention in the same slow path on the second spinlock and wait_even runtime, while I only have contention in the first one and that's why I'm more efficient (see numbers below).
Quoted 2 lines
One other thing: should you be using
spin_lock_irqsave()instead ofspin_lock_irq()in your down functions? I'm not sure it's necessary, however,
It's not necessary to save flags because they can sleep.
BTW, your rwsem-spinlock.h forgets the clear irqs in down_* and to clear irqs and save flags in the up_*! So my spinlock are penalized as the cli/sti pairs are not that light and you are providing weaker wakeup semantics than me.
My new implementation only handle up to 2^31 concurrent readers, as usual unlimited sleepers in the slow paths and down_read is not anymore recursive because of the guaranteed total fifo behaviour. This scenario will deadlock while it was working fine with the previous patches on my ftp area:
task0 task1 down_read(sem) down_write(sem) down_read(sem)
new patch is here:
ftp://ftp.us.kernel.org/pub/linux/kernel/people/andrea/patches/v2.4/2.4.4pre3/rwsem-generic-5
And now I compared 2.4.4pre3aa3 - 00_rwsem-generic-1 with 2.4.4pre3aa3 - 00_rwsem-generic-1 + rwsem-generic-5, that is the same as comparing vanilla 2.4.4pre3 with vanilla 2.4.4pre3 + rwsem-generic-5.
I wrote this rwsem stresser (if you have any bug in the rwsem this stresser will trigger it almost immediatly).
/*
* rw_semaphore benchmark (use with 2.4.3 or more recent kernels
* that uses down_read() in the page fault and down_write() in mmap). *
* Copyright (C) 2001 Andrea Arcangeli <[email protected]> SuSE */
#include <pthread.h>
#include <stdio.h>
#include <asm/page.h>
#include <sys/mman.h>
#include <asm/system.h>
#define NR_THREADS 50
#define RWSEM_LOOPS 500
#define READ_DOWN_PER_WRITE_DOWN 4
static int start;
void * rwsemflood(void * foo)
{
int i;
pthread_mutex_t * mutex = (pthread_mutex_t *) foo;
if (pthread_mutex_lock(mutex))
perror("pthread_mutex_lock"), exit(1);
for (i = 0; i < RWSEM_LOOPS; i++) {
volatile char * mem;
int i;
mem = mmap(NULL, PAGE_SIZE * READ_DOWN_PER_WRITE_DOWN,
PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
if (mem == MAP_FAILED)
perror("mmap"), exit(1);
for (i = 0; i < PAGE_SIZE * READ_DOWN_PER_WRITE_DOWN; i += PAGE_SIZE)
*(mem+i);
if (munmap((char *)mem, PAGE_SIZE) < 0)
perror("munmap"), exit(1);
}
pthread_exit(NULL);
}main()
{
pthread_t thread[NR_THREADS];
pthread_mutex_t mutex[NR_THREADS];
int i;
for (i = 0; i < NR_THREADS; i++) {
pthread_mutex_init(&mutex[i], NULL);
if (pthread_mutex_lock(&mutex[i]))
perror("pthread_mutex_lock"), exit(1);
}
for (i = 0; i < NR_THREADS; i++)
if (pthread_create(&thread[i], NULL , rwsemflood, &mutex[i]) < 0)
perror("pthread_create"), exit(1);
for (i = 0; i < NR_THREADS; i++)
if (pthread_mutex_unlock(&mutex[i]))
perror("pthread_mutex_unlock"), exit(1);
for (i = 0; i < NR_THREADS; i++)
if (pthread_join(thread[i], NULL))
perror("pthread_join"), exit(1);
}And here are the numbers with the plain 2.4.4pre3 rwsemaphores that are implemented in asm with the kernel configured for PII and the wakeup that cannot happen in an irq/softirq. The hardware is a 2-way SMP PII.
andrea@laser:~ > for i in 1 2 3 4; do time ./rwsem ;done
real 0m51.587s user 0m0.100s sys 0m52.770s
real 0m50.476s user 0m0.100s sys 0m50.730s
real 0m51.502s user 0m0.110s sys 0m53.110s
real 0m50.437s user 0m0.080s sys 0m51.070s
and now here it is the same benchmark run with rwsem-generic-5:
ndrea@laser:~ > for i in 1 2 3 4; do time ./rwsem ;done
real 0m50.035s user 0m0.080s sys 0m51.430s
real 0m50.636s user 0m0.090s sys 0m51.100s
real 0m50.038s user 0m0.050s sys 0m50.640s
real 0m50.655s user 0m0.060s sys 0m50.800s
as you can see despite it was an unfair comparison my implementation was still a bit faster or at least running at the same speed. And yes, this only benchmark the slow path, the fast path is not easy to measure from userspace and since I now inlined the fast path it must not run slower than yours. If you have more interesting bench go ahead of course.
I think you should now agree on my generic rwsemaphore implementation.
About your last patch where you try to change all archs to use your generic implementation in pre3, it still breaks during compilation and I dislike that long cryptic names:
CONFIG_RWSEM_GENERIC_SPINLOCK
CONFIG_RWSEM_XCHGADD_ALGORITHM
I don't see why you didn't call them CONFIG_RWSEM_GENERIC and CONFIG_RWSEM_ATOMIC_RETURN respectively (as I suggested originally).
I now recommend anyone with an alpha to use 2.4.4pre3 with those two patches applied:
ftp://ftp.us.kernel.org/pub/linux/kernel/people/andrea/kernels/v2.4/2.4.4pre3aa3/00_alpha-numa-3 ftp://ftp.us.kernel.org/pub/linux/kernel/people/andrea/patches/v2.4/2.4.4pre3/rwsem-generic-5
(they can be applied also on ia32 kernels of course)
About your comment on the atomic_*_return vs spinlock in the fast path, the atomic_*_return way is obviously much faster and shorter than the spinlock version on the alpha and it also saves 8 bytes in the size of the rw_semaphore compared to the generic implementation (and I'm sure the same is valid for the other RISC chips that provides read locked store conditional mechanism to implement atomic updates in memory). That's why I suggested to move it in the common code (if it would be slower not even the alpha would be trying to use it :). It's just that by sharing it we increase the userbase of the brainer part.
Also on any 64bit arch we can provide 2^32 readers and 2^32 writers at the same time without the need of a spinlock. We could do the same also on >=586 using chmxchg8b but I'm not sure if that would be a great idea.
Andrea