On Mon, Mar 14, 2022 at 12:47 AM Visa Hankala <v...@hankala.org> wrote:
> On Sun, Mar 13, 2022 at 06:26:19PM -0700, Philip Guenther wrote: > > On Sun, Mar 13, 2022 at 10:27 AM Visa Hankala <v...@hankala.org> wrote: > > > > > On Sun, Mar 13, 2022 at 04:29:44PM +0100, Mark Kettenis wrote: > > > > > ... > > > > > > Under what circumstances does memory ordering matter for these > > > > interfaces? > > > > > > Consider the following scenario: > > > > > > struct widget { > > > struct refcnt w_refcnt; > > > /* more fields spanning many cache lines */ > > > ... > > > int w_var; > > > }; > > > > > > First, CPU 1 executes: > > > > > > w->w_var = 1; > > > refcnt_rele(&w->w_refcnt); /* remains above zero */ > > > > > > > Having incremented the refcnt previous does not give this thread > exclusive > > access to 'w', so if it's writing to w->w_var then it must either > > a) have some sort of write lock taken, which it will release after this > and > > which will contain the necessary member, OR > > b) have the only access patch to this structure (i.e, it's not yet > > 'published' into structures which can be seen by other threads), in which > > case the operations which do that 'publishing' of the access to 'w' > (adding > > it to a global list, etc) must include the necessary membar. > > Lets change the sequence to this: > > local_var = atomic_load_int(&w->w_var); > refcnt_rele(&w->w_refcnt); > > Without the release barrier, is the load guaranteed to happen before > the reference count is decremented? > That's completely uncomparable to what you described before for "CPU 1" because there's no write to anything but the refcnt. If no one writes to the object that is ref counted, then there are no visibility problems, no? > Next, CPU 2 executes: > > > > > > if (refcnt_rele(&w->w_refcnt)) /* refcnt drops to zero */ > > > free(w); > > > > > > > How did CPU 2 get what is now exclusive access to 'w' without any > membars? > > If that's possible then it was just accessing 'w' and possibly not seeing > > the update to w->w_var even _before_ the refcnt_rele(), so putting a > membar > > in refcnt_rele() hides the incorrect code by suppressing the later crash! > > > > If these membars appear to help then the code is and remains broken. > This > > change should not be done. > > It is not uncommon to see something like below: > > Access object with read intent: > > mtx_enter(&w_lock); > w = lookup_from_list(); > if (w != NULL) > refcnt_take(&w->w_refcnt); > mtx_leave(&w_lock); > if (w == NULL) > return; > ... > No writes to *w described *OR LEGAL*. > if (refcnt_rele(&w->w_refcnt)) > free(w); Delete object: > > mtx_enter(&w_lock); > w = lookup_from_list(); > if (w != NULL) > remove_from_list(w); > mtx_leave(&w_lock); > This does the 'release' on the change to w's list link(s). > /* Release list's reference. */ > if (w != NULL && refcnt_rele(&w->w_refcnt)) > free(w); > > Above, any refcnt_rele() can release the final reference. > > If there is no acquire barrier after the refcnt 1->0 transition, what > is known about the CPU's local view of the object after refcnt_rele()? > Okay, if refcnt is used with objects that have embedded list links, or could be, then refcnt_rele needs acquire semantics on the 1->0 transition. I can agree with your justification for that. Do you have a similar example for giving it release semantics as your diff proposed? What's the otherwise-unprotected-by-synchronization-primitive that has to be protected? > The decrement operation in the <linux/refcount.h> API of Linux and > the <sys/refcount.h> API of FreeBSD provide release and acquire > barriers. Are they wrong? > I'm not sure what argument you're making here. So far, I've understood this to be "here's why this API needs to provide these semantics, as justified by the logic of acquire/release semantics and <these> examples". Per above, I think you've justified giving it acquire semantics, but don't understand the justification for also giving it release semantics. Now you're giving a completely different justification: "they did so". That's a justification under any of (at least) three lines: a) they're incapable of making mistakes b) you've read the docs/explanation behind their decisions and were persuaded that it's universally true that such an API needs these semantics to be usable c) this API is shared with other OS'es from which we import code or developer experience and it's not worth the CPU time for us to optimize it when they haven't d) some other logic for why their doing it means it's right for us to do it too? I can probably buy (b) or (c)....if that's the argument you're making, but then I'd rather it be clear we're doing this on trust and not on our collective understanding on what breaks when this isn't done. (The loss on doing that is that the examples of failures are how we all build our understanding and intuition for( what's correct or not for membar use. The kernel would not be less reliable if we made *all* primitives have both release and acquire semantics...but we don't, because we collectively believe that some don't need one or the other. If we give up on understanding why in this case, it'll be harder to understand why others are different.) Philip Guenther