Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reference-barrier to (srfi 124) #796

Merged
merged 1 commit into from
Dec 14, 2021
Merged

Conversation

dpk
Copy link
Contributor

@dpk dpk commented Dec 14, 2021

According to a post-finalization note in SRFI 124, this procedure is now mandatory. Its purpose is not entirely clear to me (nor was it to Vyzo when I implemented SRFI 124 for Gerbil), but I think this should be an acceptable implementation for Chibi’s GC.

@ashinn ashinn merged commit 50188a6 into ashinn:master Dec 14, 2021
@mnieper
Copy link
Collaborator

mnieper commented Dec 14, 2021

An example program where you need it is given here: https://srfi-email.schemers.org/srfi-124/msg/4181743/

(Please excuse the partly broken code indentation.)

@ashinn
Copy link
Owner

ashinn commented Dec 14, 2021

For the record, I haven't been following the SRFI discussion but it seems to me that if something like reference-barrier is needed then the design of ephemerons is broken.

@mnieper
Copy link
Collaborator

mnieper commented Dec 14, 2021

(import (srfi 124))

gives: ERROR on line 6 of file lib/srfi/124.sld: undefined variable: if

Please fix the imports (at least define and if).

Independently, consider the following code:

(let* ((k (string #\k))
       (e (make-ephemeron k 42))
       (v (ephemeron-datum e)))
  (reference-barrier k)
  v))

It must return 42 because the ephemeron cannot be broken before the reference barrier.

Does Chibi under any circumstance and under any optimization settings happen to inline the call to reference-barrier? In that case, the above expression would be equivalent to

(let* ((k (string #\k))
       (e (make-ephemeron k 42))
       (v (ephemeron-datum e)))
  (if #f #f)
  v))

and thus potentially equivalent to

(let* ((k (string #\k))
       (e (make-ephemeron k 42))
       (v (begin (gc) (ephemeron-datum e))))
  (if #f #f)
  v))

which can, in principle, evaluate to #f if k is ever deleted from the closure of the lambda binding e. Again it depends on what optimizations Chibi is capable of. Alex will be able to tell that.

If on the other side, the call to reference-barrier is opaque to the Chibi interpreter and Chibi will never optimize away the call, Daphne's solution looks good to me.

@mnieper
Copy link
Collaborator

mnieper commented Dec 14, 2021

For the record, I haven't been following the SRFI discussion but it seems to me that if something like reference-barrier is needed then the design of ephemerons is broken.

Can you clarify? For sure, it would be nicer if we need fewer primitives. It has been consensus on the SRFI 124 mailing list that reference-barrier is needed (with the current design).

What do you exactly mean by that the design is broken? Instead of reference-barrier, we could have alternative forms like

(call-referencing key thunk)

calling thunk and returning its results and guaranteeing that key is not garbage collected during the dynamic extent of the call.

But I am not sure whether you would consider such a primitive less broken.

@dpk
Copy link
Contributor Author

dpk commented Dec 14, 2021

Please fix the imports (at least define and if).

Grumblegrumblegrumble. (Yes, sorry, I didn’t test this before merging. I consider myself fired. I had to do a (cond-expand (chibi (define (reference-barrier k) (if #f #f)))) in some other code I was writing, and just copied that in without thinking about the import situation.)

@dpk
Copy link
Contributor Author

dpk commented Dec 14, 2021

For the record, I haven't been following the SRFI discussion but it seems to me that if something like reference-barrier is needed then the design of ephemerons is broken.

Since I now somewhat understand the need for it, it seems to me that the issue which reference-barrier resolves could arise in any implementation of weak-referencing boxes.

If the design needs to be fixed at all, it might be by providing syntax which evaluates a body and automatically uses reference-barrier at the end, or similar.

@mnieper
Copy link
Collaborator

mnieper commented Dec 14, 2021

For the record, I haven't been following the SRFI discussion but it seems to me that if something like reference-barrier is needed then the design of ephemerons is broken.

Since I now somewhat understand the need for it, it seems to me that the issue which reference-barrier resolves could arise in any implementation of weak-referencing boxes.

Yes, it is the same situation for (ordinary) weak pairs.

If the design needs to be fixed at all, it might be by providing syntax which evaluates a body and automatically uses reference-barrier at the end, or similar.

Do you mean something like my call-referencing from above, but as syntax?

@dpk
Copy link
Contributor Author

dpk commented Dec 14, 2021

Do you mean something like my call-referencing from above, but as syntax?

Yes, I hadn’t seen your reply yet when I started typing mine.

@ashinn
Copy link
Owner

ashinn commented Dec 17, 2021

My general complaint is that this violates the first sentence of the introduction to the standard. Instead of making ephemerons "just work," we're introducing a confusing and error prone construct.

I haven't dug into how difficult it is to fix the root problem, but ephemerons already require special GC support. If nothing else, adding a volatile annotation as supported by C compilers to the ephemeron accessors would disable reordering.

Failing that, if we really need a stopgap solution for practical implementations now, then we can just unify the API into a single construct that avoids this problem:

(call-with-ephemeron eph (lambda (value broken?) ...)

optionally with some syntactic sugar:

(ephemeron-case eph
  ((live value) ... code using value ...)
  (broken ... code for broken case ...))

@mnieper
Copy link
Collaborator

mnieper commented Dec 17, 2021

I agree that using SRFI 124's API correctly, which is basically a stripped-down version of MIT/GNU Scheme's API, can somewhat be as delicate as writing a multi-threaded program correctly.

Let me cite the example from SRFI 124 here:

;; Return the datum component of the ephemeron if its key component is
KEY. Return #f otherwise.
(define (ephemeron-ref ephemeron key)
  (let ((k (ephemeron-key ephemeron))
        (d (ephemeron-datum ephemeron)))
    (and (not (ephemeron-broken? ephemeron))
         (eq? key k)
         (reference-barrier k)
         d)))

Here, an optimizing compiler can reorder the side-effect free (eq? key k) before (ephemeron-broken? ephemeron) (meaning that the ephemeron may be broken too early) so that disabling reordering with respect to the accessors ephemeron-key and ephemeron-datum wouldn't help.

This suggests redefining ephemeron-broken? so that reordering across it won't be possible. I don't see how this can be implemented efficiently in an optimizing compiler. Forbidding reordering of any expression across such a call basically means that all expressions have to be considered not without side effects. Note that none of the memory order primitives of the C (and other) threading models have that questionable power. What we would actually need is to mark the reference of key in (eq? key k) volatile. In some sense, this is what reference-barrier already does, but it could be clearer if the API were so that we would write (eq? (volatile key) k) with the semantics of

(define-syntax volatile
  (syntax-rules ()
    ((volatile var)
     (let ((val var)) 
       (reference-barrier val) 
       val))))

But such is just a reformulation of the current API so does not really address your point.

The (syntactic) construct you suggest as an alternative is possibly implemented (modulo some syntactic variations) as

(define-syntax ephemeron-case
  (syntax-rules (ephemeron else)
    ((ephemeron-case eph-expr
      ((ephemeron key datum) . body1)
      (else . body2))
     (let ((eph eph-expr))    
       (let ((k (ephemeron-key eph)) (d (ephemeron-datum eph)))
         (let ((broken? (ephemeron-broken? eph)))
           (reference-barrier k)
           (if broken? 
               (let* () . body2)
               (let ((key k) (datum d)) . body1))))))))

With it, the example of ephemeron-ref would be rewritten as

(define (ephemeron-ref eph key)
  (ephemeron-case eph
    ((ephemeron k d)
     (and (eq? key k) d))
    (else #f)))

It expands to (modulo irrelevant simplifications)

(define (ephemeron-ref eph key)
  (let ((k (ephemeron-key eph))
        (d (ephemeron-key eph)))
    (let ((broken? (ephemeron-broken? eph)))
      (reference-barrier k)
      (and (not broken?)
              (eq? key k) d))))

This looks correct but isn't. An implementation would be free to rewrite this to

(define (ephemeron-ref eph key)
  (define key-eq? <one-argument procedure that checks whether the argument is eq? to key>)
  (let ((k (ephemeron-key eph))
        (d (ephemeron-key eph)))
    (let ((broken? (ephemeron-broken? eph)))
      (reference-barrier k)
      (and (not broken?)
              (key-eq? k) d))))

The "one-argument procedure" doesn't necessarily have to keep a (strong) reference to key as it may not be necessary, depending on what the value of key is. But then, when there is no other reference to key in the continuation of the call to ephemeron-ref, the ephemeron may be broken too early.

In fact, (reference-barrier key) is what actually gives meaning to SRFI 124's terminology "strongly reachable": A value is per definition strongly reachable at the evaluation of an expression if reference-barrier is invoked on it in the continuation of that evaluation. Everything else is probably not well-defined by the Scheme semantics.

@ashinn
Copy link
Owner

ashinn commented Dec 17, 2021

The compiler could internally (transparently to the user) treat ephemeron accessors as volatile/unreorderable. That doesn't disable code reordering in general, and doesn't preclude future compiler optimizations from figuring out when it actually could reorder the ephemeron accessors.

But failing that, combining all of the ephemeron operations into a single primitive could be built on top of existing systems (using reference-barrier under the hood if needed), without exposing any of the details to the user. The ephemeron-ref example is just:

(define (ephemeron-ref ephemeron key)
  (ephemeron-case ephemeron
    ((live k v) (and (eq? k key) v))
    (broken #f)))

@mnieper
Copy link
Collaborator

mnieper commented Dec 17, 2021

The compiler could internally (transparently to the user) treat ephemeron accessors as volatile/unreorderable. That doesn't disable code reordering in general, and doesn't preclude future compiler optimizations from figuring out when it actually could reorder the ephemeron accessors.

Can you explain what you mean by the accessors being "unreorderable"? Unreordable with respect to what? If we insert "every other expression" for "what", it would be far too strong.

But failing that, combining all of the ephemeron operations into a single primitive could be built on top of existing systems (using reference-barrier under the hood if needed), without exposing any of the details to the user. The ephemeron-ref example is just:

(define (ephemeron-ref ephemeron key)
  (ephemeron-case ephemeron
    ((live k v) (and (eq? k key) v))
    (broken #f)))

See the second part of my post why this would still not be correct. (As I wrote: dealing with ephemerons can be as tricky as getting multi-threaded code right).

@ashinn
Copy link
Owner

ashinn commented Dec 17, 2021

The compiler argument is just a vague handwavy way of saying "it should be possible." As one example I was suggesting to prevent code reordering of ephemeron operations where they happen locally (because code reordering only happens locally, modulo inlining). Transitive calls to ephemeron operations are possible, if pathological, and may defeat this, which is perhaps an argument not to separate the ephemeron operations to begin with.

Sorry I missed your ephemeron-case example. I of course meant ephemeron-case to actually be implemented correctly, with any necessary compiler support. Here it suffices to move the reference barrier to the end of the clause.

Anyway, I'm just grumbling that we're piling on features and making things more complicated rather than simpler, but I don't have time to spend on the simpler solution. Please don't take that as an argument that the simpler solution doesn't exist.

@mnieper
Copy link
Collaborator

mnieper commented Dec 17, 2021

The compiler argument is just a vague handwavy way of saying "it should be possible." As one example I was suggesting to prevent code reordering of ephemeron operations where they happen locally (because code reordering only happens locally, modulo inlining). Transitive calls to ephemeron operations are possible, if pathological, and may defeat this, which is perhaps an argument not to separate the ephemeron operations to begin with.

Sorry I missed your ephemeron-case example. I of course meant ephemeron-case to actually be implemented correctly, with any necessary compiler support. Here it suffices to move the reference barrier to the end of the clause.

The root problem is not the separation of the ephemeron operations but the requirement that the inconspicuously looking expression (eq? key k) actually references the key where it appears in the code. Ephemeron-case cannot know about that expression so it doesn't help here.

Anyway, I'm just grumbling that we're piling on features and making things more complicated rather than simpler, but I don't have time to spend on the simpler solution. Please don't take that as an argument that the simpler solution doesn't exist.

I get it. I would also like to see a simpler solution but so far it seems to me that a solution without explicit reference barriers is not possible without making ephemeron trivial (and without implicitly inserting a reference barrier at every value reference for every value).

What I mean by this: For ephemerons (or, more generally, weak references) to make any sense, the notion of strongly reachable must be defined in the language semantics. SRFI 124 (implicitly) does this through reference-barrier (as I wrote in my longer post from above); without the primitive notion of a reference barrier, the only other meaning of "strongly reachable" I can think of is that every access to the store (as defined in the formal R7RS denotational semantics) counts as a strong reference. But this would mean that an optimizing compiler would have to treat any access to the store as a non-side-effect-free operation, which would be, in the other model, equivalent to calling reference-barrier after every lookup in the store. Such a thing would be devastating to optimization.

@mnieper mnieper mentioned this pull request Dec 31, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants