atomicity of non-local returns

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
6 messages Options
Reply | Threaded
Open this post in threaded view
|

atomicity of non-local returns

Ben Coman
I understand generally that an inlined  #ifTrue  is atomic.  For
example, here after the primitive returns, no interruption can occur
before the #ensure is invoked...
    self primitiveWaitAcquire ifTrue:
           [mutuallyExclusiveBlock ensure: [self release]].

But I want to know whether a non-local return could have an impact on
that atomicity. For example ...
    self primitiveWaitAcquire ifTrue:
           [ ^ mutuallyExclusiveBlock ensure: [self release]]

cheers -ben

Reply | Threaded
Open this post in threaded view
|

Re: atomicity of non-local returns

Clément Béra
Hi Ben,

Firstly be careful because atomic usually means that the thread cannot be interrupted by a concurrent native thread, so an atomic operation has to be guaranteed at processor level.

Assuming that by atomic you mean that the operation's process cannot be preempted by another green thread, yes there is big problems with non local returns.

Two main issues:
- NLR can trigger unwind blocks, which can switch to another process.
- NLR can fail, triggering the cannotReturn: call-back, which can switch to another process.

I understand generally that an inlined  #ifTrue  is atomic.  For
example, here after the primitive returns, no interruption can occur
before the #ensure is invoked...
    self primitiveWaitAcquire ifTrue:
           [mutuallyExclusiveBlock ensure: [self release]].


The exact answer is that #ifTrue: can be interrupted if self primitiveWaitAcquire is not a boolean, and can't be interrupted if it's a boolean. It's nice to mark in a comment if part of the method's code has not to be interrupted to avoid issues years later.

But I want to know whether a non-local return could have an impact on
that atomicity. For example ...
    self primitiveWaitAcquire ifTrue:
           [ ^ mutuallyExclusiveBlock ensure: [self release]]


In your code there is no non local return because ifTrue: is inlined by the Bytecode compiler. Try to compile it and look at the bytecode you will see.

Else as I said at the beginning of the mail, a non local return can imply a process switch if that was your question.





 

On Mon, Jun 6, 2016 at 2:46 PM, Ben Coman <[hidden email]> wrote:
I understand generally that an inlined  #ifTrue  is atomic.  For
example, here after the primitive returns, no interruption can occur
before the #ensure is invoked...
    self primitiveWaitAcquire ifTrue:
           [mutuallyExclusiveBlock ensure: [self release]].

But I want to know whether a non-local return could have an impact on
that atomicity. For example ...
    self primitiveWaitAcquire ifTrue:
           [ ^ mutuallyExclusiveBlock ensure: [self release]]

cheers -ben


Reply | Threaded
Open this post in threaded view
|

Re: atomicity of non-local returns

Ben Coman
On Mon, Jun 6, 2016 at 9:34 PM, Clément Bera <[hidden email]> wrote:
> Hi Ben,
>
> Firstly be careful because atomic usually means that the thread cannot be
> interrupted by a concurrent native thread, so an atomic operation has to be
> guaranteed at processor level.
>
> Assuming that by atomic you mean that the operation's process cannot be
> preempted by another green thread, yes there is big problems with non local
> returns.

Yes, I meant at the image level, run by the single-thread VM.

>
> Two main issues:
> - NLR can trigger unwind blocks, which can switch to another process.
> - NLR can fail, triggering the cannotReturn: call-back, which can switch to
> another process.

These would presumably could only occur after the #ensure: had been invoked?  .

>
> I understand generally that an inlined  #ifTrue  is atomic.  For
> example, here after the primitive returns, no interruption can occur
> before the #ensure is invoked...
>     self primitiveWaitAcquire ifTrue:
>            [mutuallyExclusiveBlock ensure: [self release]].
>
> The exact answer is that #ifTrue: can be interrupted if self
> primitiveWaitAcquire is not a boolean, and can't be interrupted if it's a
> boolean. It's nice to mark in a comment if part of the method's code has not
> to be interrupted to avoid issues years later.

Good tip.

>
> But I want to know whether a non-local return could have an impact on
> that atomicity. For example ...
>     self primitiveWaitAcquire ifTrue:
>            [ ^ mutuallyExclusiveBlock ensure: [self release]]
>
> In your code there is no non local return because ifTrue: is inlined by the
> Bytecode compiler.

I'm not clear on your meaning.  If there were additional code
following those two lines, then they would not be executed, so it
would still be a NLR ?  I took my understanding from [1].  And in the
bytecode below it seems #returnTop is associated with the NLRs - so
the NLR is still there(?), but having looked at the bytecode I can now
see it has no impact, I think either with or without inlining makes no
difference wrt NLR.


[1] https://silversmalltalk.wordpress.com/2011/02/02/implementing-smalltalks-non-local-returns-in-javascript/

> Try to compile it and look at the bytecode you will see.

Good idea. So I share a little analysis...


The bytecode for these two lines is identical,
    a blah: [ [ self foo ] ensure: [  self bar  ] ]
    a blah: [ ^ [ self foo ] ensure: [ self bar ] ]
except for bytecode 49, respectively...
     "49 <7D> blockReturn"
     "49 <7C> returnTop"

"29 <10> pushTemp: 0"
"30 <8F 00 00 10> closureNumCopied: 0 numArgs: 0 bytes 34 to 49"
"34 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 38 to 40"
"38 <70> self"
"39 <D0> send: foo"
"40 <7D> blockReturn"
"41 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 45 to 47"
"45 <70> self"
"46 <D1> send: bar"
"47 <7D> blockReturn"
"48 <E2> send: ensure:"
"49 <7C> returnTop"
"50 <E3> send: blah:"
"51 <87> pop"
"52 <78> returnSelf"


Changing #blah: to #ifTrue:  these two lines
     a ifTrue: [ [ self foo ] ensure: [ self bar ] ]
     a ifTrue: [ ^ [ self foo ] ensure: [ self bar ] ]
are the same except for bytecode 49, respectively...
     "47 <87>  pop"
     "47 <7C> returnTop"

"29 <10> pushTemp: 0"
"30 <AC 10> jumpFalse: 48"
"32 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 36 to 38"
"36 <70> self"
"37 <D0> send: foo"
"38 <7D> blockReturn"
"39 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 43 to 45"
"43 <70> self"
"44 <D1> send: bar"
"45 <7D> blockReturn"
"46 <E2> send: ensure:"
"47 <87> pop"
"48 <78> returnSelf"

And between #blah: and #ifTrue: the difference is
removal of...
    "30 <8F 00 00 10> closureNumCopied: 0 numArgs: 0 bytes 34 to 49"
    "49 <7D> blockReturn"
    "50 <E3> send: blah:"
and replaced them with...
    "30 <AC 10> jumpFalse: 48"

It took me a while to guess how to follow the execution path.   I had
assumed it would just go top to bottom, but that didn't make sense for
foo to be executed before blah.  Can you confirm.. I guess the trick
(for #blah) is...

1. At 29, push the temp
2. skip the closureNumCopied bytes 34 to 49,
3. At 50, send #blah.
4. next is bytecode 34 skips bytecodes 38 to 40,
5. and bytecode 41 skips bytecodes 45 to 47
6. At 48 send #ensure:.

versus   #ifTrue:  bytecocde...

1. At 29, push the temp
2. skip closureNumCopied bytes  36 to 38"
3. skip closureNumCopied: bytes 43 to 45"
4. At 46, send #ensure:

So its clear (IIUC) that with  #IfTrue:  the #ensure is the first send:
after the push temp, but the non-inlined  #blah:  is an extra send
between which could be interrupted and prevent the #ensure from being
executed.

>
> Else as I said at the beginning of the mail, a non local return can imply a
> process switch if that was your question.

Yes, but a process switch is okay after the send of #ensure:
just not before.

thx. cheers -ben

>
>
>
>
>
>
>
> On Mon, Jun 6, 2016 at 2:46 PM, Ben Coman <[hidden email]> wrote:
>>
>> I understand generally that an inlined  #ifTrue  is atomic.  For
>> example, here after the primitive returns, no interruption can occur
>> before the #ensure is invoked...
>>     self primitiveWaitAcquire ifTrue:
>>            [mutuallyExclusiveBlock ensure: [self release]].
>>
>> But I want to know whether a non-local return could have an impact on
>> that atomicity. For example ...
>>     self primitiveWaitAcquire ifTrue:
>>            [ ^ mutuallyExclusiveBlock ensure: [self release]]
>>
>> cheers -ben
>>
>

Reply | Threaded
Open this post in threaded view
|

Re: atomicity of non-local returns

Clément Béra
Yeah anyway the non local return happens after the ensure: send, so nothing to worry about.

In your analysis, you need to see that the #returnTop bytecode means NLR in a block but method return in a method. Even if it looks the same, in one case the #returnTop belongs to a block closure bytecode, hence is a NLR, whereas in the othercase, it belongs to the method bytecode, hence it's a method return.

See StackInterpreter>>commonReturn, this code:

"If this is a method simply return to the  sender/caller."
(self iframeIsBlockActivation: localFP) ifFalse:
[^self commonCallerReturn].

test if the return top is a method return or block NLR. I don't know why Eliot decided to share the bytecode between method return and block NLR, but there are 3 returns in Smalltalk, method return, block return and block NLR and they're all quite different.

About your example, the normal closure bytecode creates the closure, pushes it on top of the stack, then skips over the closure bytecode (jump forward). About the jump instructions generated by ifTrue:ifFalse:, the jump forward instruction has no interrupt point, while the conditional jump have an interrupt point only in the case of a must be boolean. Does this make sense ? In Squeak there's indentation in bytecode printing to ease control flow understanding, that may help you. You are using a Squeak image for VM simulation and Pharo image to implement the primitives and experiment with the primitives, aren't you ? 

Cheers

On Mon, Jun 6, 2016 at 6:34 PM, Ben Coman <[hidden email]> wrote:
On Mon, Jun 6, 2016 at 9:34 PM, Clément Bera <[hidden email]> wrote:
> Hi Ben,
>
> Firstly be careful because atomic usually means that the thread cannot be
> interrupted by a concurrent native thread, so an atomic operation has to be
> guaranteed at processor level.
>
> Assuming that by atomic you mean that the operation's process cannot be
> preempted by another green thread, yes there is big problems with non local
> returns.

Yes, I meant at the image level, run by the single-thread VM.

>
> Two main issues:
> - NLR can trigger unwind blocks, which can switch to another process.
> - NLR can fail, triggering the cannotReturn: call-back, which can switch to
> another process.

These would presumably could only occur after the #ensure: had been invoked?  .

>
> I understand generally that an inlined  #ifTrue  is atomic.  For
> example, here after the primitive returns, no interruption can occur
> before the #ensure is invoked...
>     self primitiveWaitAcquire ifTrue:
>            [mutuallyExclusiveBlock ensure: [self release]].
>
> The exact answer is that #ifTrue: can be interrupted if self
> primitiveWaitAcquire is not a boolean, and can't be interrupted if it's a
> boolean. It's nice to mark in a comment if part of the method's code has not
> to be interrupted to avoid issues years later.

Good tip.

>
> But I want to know whether a non-local return could have an impact on
> that atomicity. For example ...
>     self primitiveWaitAcquire ifTrue:
>            [ ^ mutuallyExclusiveBlock ensure: [self release]]
>
> In your code there is no non local return because ifTrue: is inlined by the
> Bytecode compiler.

I'm not clear on your meaning.  If there were additional code
following those two lines, then they would not be executed, so it
would still be a NLR ?  I took my understanding from [1].  And in the
bytecode below it seems #returnTop is associated with the NLRs - so
the NLR is still there(?), but having looked at the bytecode I can now
see it has no impact, I think either with or without inlining makes no
difference wrt NLR.


[1] https://silversmalltalk.wordpress.com/2011/02/02/implementing-smalltalks-non-local-returns-in-javascript/

> Try to compile it and look at the bytecode you will see.

Good idea. So I share a little analysis...


The bytecode for these two lines is identical,
    a blah: [ [ self foo ] ensure: [  self bar  ] ]
    a blah: [ ^ [ self foo ] ensure: [ self bar ] ]
except for bytecode 49, respectively...
     "49 <7D> blockReturn"
     "49 <7C> returnTop"

"29 <10> pushTemp: 0"
"30 <8F 00 00 10> closureNumCopied: 0 numArgs: 0 bytes 34 to 49"
"34 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 38 to 40"
"38 <70> self"
"39 <D0> send: foo"
"40 <7D> blockReturn"
"41 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 45 to 47"
"45 <70> self"
"46 <D1> send: bar"
"47 <7D> blockReturn"
"48 <E2> send: ensure:"
"49 <7C> returnTop"
"50 <E3> send: blah:"
"51 <87> pop"
"52 <78> returnSelf"


Changing #blah: to #ifTrue:  these two lines
     a ifTrue: [ [ self foo ] ensure: [ self bar ] ]
     a ifTrue: [ ^ [ self foo ] ensure: [ self bar ] ]
are the same except for bytecode 49, respectively...
     "47 <87>  pop"
     "47 <7C> returnTop"

"29 <10> pushTemp: 0"
"30 <AC 10> jumpFalse: 48"
"32 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 36 to 38"
"36 <70> self"
"37 <D0> send: foo"
"38 <7D> blockReturn"
"39 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 43 to 45"
"43 <70> self"
"44 <D1> send: bar"
"45 <7D> blockReturn"
"46 <E2> send: ensure:"
"47 <87> pop"
"48 <78> returnSelf"

And between #blah: and #ifTrue: the difference is
removal of...
    "30 <8F 00 00 10> closureNumCopied: 0 numArgs: 0 bytes 34 to 49"
    "49 <7D> blockReturn"
    "50 <E3> send: blah:"
and replaced them with...
    "30 <AC 10> jumpFalse: 48"

It took me a while to guess how to follow the execution path.   I had
assumed it would just go top to bottom, but that didn't make sense for
foo to be executed before blah.  Can you confirm.. I guess the trick
(for #blah) is...

1. At 29, push the temp
2. skip the closureNumCopied bytes 34 to 49,
3. At 50, send #blah.
4. next is bytecode 34 skips bytecodes 38 to 40,
5. and bytecode 41 skips bytecodes 45 to 47
6. At 48 send #ensure:.

versus   #ifTrue:  bytecocde...

1. At 29, push the temp
2. skip closureNumCopied bytes  36 to 38"
3. skip closureNumCopied: bytes 43 to 45"
4. At 46, send #ensure:

So its clear (IIUC) that with  #IfTrue:  the #ensure is the first send:
after the push temp, but the non-inlined  #blah:  is an extra send
between which could be interrupted and prevent the #ensure from being
executed.

>
> Else as I said at the beginning of the mail, a non local return can imply a
> process switch if that was your question.

Yes, but a process switch is okay after the send of #ensure:
just not before.

thx. cheers -ben

>
>
>
>
>
>
>
> On Mon, Jun 6, 2016 at 2:46 PM, Ben Coman <[hidden email]> wrote:
>>
>> I understand generally that an inlined  #ifTrue  is atomic.  For
>> example, here after the primitive returns, no interruption can occur
>> before the #ensure is invoked...
>>     self primitiveWaitAcquire ifTrue:
>>            [mutuallyExclusiveBlock ensure: [self release]].
>>
>> But I want to know whether a non-local return could have an impact on
>> that atomicity. For example ...
>>     self primitiveWaitAcquire ifTrue:
>>            [ ^ mutuallyExclusiveBlock ensure: [self release]]
>>
>> cheers -ben
>>
>


Reply | Threaded
Open this post in threaded view
|

Re: atomicity of non-local returns

Ben Coman
On Tue, Jun 7, 2016 at 2:39 AM, Clément Bera <[hidden email]> wrote:

> Yeah anyway the non local return happens after the ensure: send, so nothing
> to worry about.
>
> In your analysis, you need to see that the #returnTop bytecode means NLR in
> a block but method return in a method. Even if it looks the same, in one
> case the #returnTop belongs to a block closure bytecode, hence is a NLR,
> whereas in the othercase, it belongs to the method bytecode, hence it's a
> method return.
>
> See StackInterpreter>>commonReturn, this code:
>
> "If this is a method simply return to the  sender/caller."
> (self iframeIsBlockActivation: localFP) ifFalse:
> [^self commonCallerReturn].
>

Ahhh, got it.  Quite a subtle semantic when the source code looks much the same.


> test if the return top is a method return or block NLR. I don't know why
> Eliot decided to share the bytecode between method return and block NLR, but
> there are 3 returns in Smalltalk, method return, block return and block NLR
> and they're all quite different.
>
> About your example, the normal closure bytecode creates the closure, pushes
> it on top of the stack, then skips over the closure bytecode (jump forward).
> About the jump instructions generated by ifTrue:ifFalse:, the jump forward
> instruction has no interrupt point, while the conditional jump have an
> interrupt point only in the case of a must be boolean. Does this make sense
> ?

Just to confirm in my own words...
the jumpFalse:
* sent to a boolean has no interrupt point
* sent to a non-boolean has an interrupt point

In Squeak there's indentation in bytecode printing to ease control flow
> understanding, that may help you. You are using a Squeak image for VM
> simulation and Pharo image to implement the primitives and experiment with
> the primitives, aren't you ?

The purpose of the primitives is for Pharo, but right this moment I'm
using Squeak for both the simulator and the image being simulated (my
changeset only needed a few changes to split Context back to its
earlier components).  While learning the simulator it was the path of
least resistance using the supplied scripts to build the reader image,
and I hadn't swapped to simulating a Pharo image yet.

cheers -ben

>
> Cheers
>
> On Mon, Jun 6, 2016 at 6:34 PM, Ben Coman <[hidden email]> wrote:
>>
>> On Mon, Jun 6, 2016 at 9:34 PM, Clément Bera <[hidden email]>
>> wrote:
>> > Hi Ben,
>> >
>> > Firstly be careful because atomic usually means that the thread cannot
>> > be
>> > interrupted by a concurrent native thread, so an atomic operation has to
>> > be
>> > guaranteed at processor level.
>> >
>> > Assuming that by atomic you mean that the operation's process cannot be
>> > preempted by another green thread, yes there is big problems with non
>> > local
>> > returns.
>>
>> Yes, I meant at the image level, run by the single-thread VM.
>>
>> >
>> > Two main issues:
>> > - NLR can trigger unwind blocks, which can switch to another process.
>> > - NLR can fail, triggering the cannotReturn: call-back, which can switch
>> > to
>> > another process.
>>
>> These would presumably could only occur after the #ensure: had been
>> invoked?  .
>>
>> >
>> > I understand generally that an inlined  #ifTrue  is atomic.  For
>> > example, here after the primitive returns, no interruption can occur
>> > before the #ensure is invoked...
>> >     self primitiveWaitAcquire ifTrue:
>> >            [mutuallyExclusiveBlock ensure: [self release]].
>> >
>> > The exact answer is that #ifTrue: can be interrupted if self
>> > primitiveWaitAcquire is not a boolean, and can't be interrupted if it's
>> > a
>> > boolean. It's nice to mark in a comment if part of the method's code has
>> > not
>> > to be interrupted to avoid issues years later.
>>
>> Good tip.
>>
>> >
>> > But I want to know whether a non-local return could have an impact on
>> > that atomicity. For example ...
>> >     self primitiveWaitAcquire ifTrue:
>> >            [ ^ mutuallyExclusiveBlock ensure: [self release]]
>> >
>> > In your code there is no non local return because ifTrue: is inlined by
>> > the
>> > Bytecode compiler.
>>
>> I'm not clear on your meaning.  If there were additional code
>> following those two lines, then they would not be executed, so it
>> would still be a NLR ?  I took my understanding from [1].  And in the
>> bytecode below it seems #returnTop is associated with the NLRs - so
>> the NLR is still there(?), but having looked at the bytecode I can now
>> see it has no impact, I think either with or without inlining makes no
>> difference wrt NLR.
>>
>>
>> [1]
>> https://silversmalltalk.wordpress.com/2011/02/02/implementing-smalltalks-non-local-returns-in-javascript/
>>
>> > Try to compile it and look at the bytecode you will see.
>>
>> Good idea. So I share a little analysis...
>>
>>
>> The bytecode for these two lines is identical,
>>     a blah: [ [ self foo ] ensure: [  self bar  ] ]
>>     a blah: [ ^ [ self foo ] ensure: [ self bar ] ]
>> except for bytecode 49, respectively...
>>      "49 <7D> blockReturn"
>>      "49 <7C> returnTop"
>>
>> "29 <10> pushTemp: 0"
>> "30 <8F 00 00 10> closureNumCopied: 0 numArgs: 0 bytes 34 to 49"
>> "34 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 38 to 40"
>> "38 <70> self"
>> "39 <D0> send: foo"
>> "40 <7D> blockReturn"
>> "41 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 45 to 47"
>> "45 <70> self"
>> "46 <D1> send: bar"
>> "47 <7D> blockReturn"
>> "48 <E2> send: ensure:"
>> "49 <7C> returnTop"
>> "50 <E3> send: blah:"
>> "51 <87> pop"
>> "52 <78> returnSelf"
>>
>>
>> Changing #blah: to #ifTrue:  these two lines
>>      a ifTrue: [ [ self foo ] ensure: [ self bar ] ]
>>      a ifTrue: [ ^ [ self foo ] ensure: [ self bar ] ]
>> are the same except for bytecode 49, respectively...
>>      "47 <87>  pop"
>>      "47 <7C> returnTop"
>>
>> "29 <10> pushTemp: 0"
>> "30 <AC 10> jumpFalse: 48"
>> "32 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 36 to 38"
>> "36 <70> self"
>> "37 <D0> send: foo"
>> "38 <7D> blockReturn"
>> "39 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 43 to 45"
>> "43 <70> self"
>> "44 <D1> send: bar"
>> "45 <7D> blockReturn"
>> "46 <E2> send: ensure:"
>> "47 <87> pop"
>> "48 <78> returnSelf"
>>
>> And between #blah: and #ifTrue: the difference is
>> removal of...
>>     "30 <8F 00 00 10> closureNumCopied: 0 numArgs: 0 bytes 34 to 49"
>>     "49 <7D> blockReturn"
>>     "50 <E3> send: blah:"
>> and replaced them with...
>>     "30 <AC 10> jumpFalse: 48"
>>
>> It took me a while to guess how to follow the execution path.   I had
>> assumed it would just go top to bottom, but that didn't make sense for
>> foo to be executed before blah.  Can you confirm.. I guess the trick
>> (for #blah) is...
>>
>> 1. At 29, push the temp
>> 2. skip the closureNumCopied bytes 34 to 49,
>> 3. At 50, send #blah.
>> 4. next is bytecode 34 skips bytecodes 38 to 40,
>> 5. and bytecode 41 skips bytecodes 45 to 47
>> 6. At 48 send #ensure:.
>>
>> versus   #ifTrue:  bytecocde...
>>
>> 1. At 29, push the temp
>> 2. skip closureNumCopied bytes  36 to 38"
>> 3. skip closureNumCopied: bytes 43 to 45"
>> 4. At 46, send #ensure:
>>
>> So its clear (IIUC) that with  #IfTrue:  the #ensure is the first send:
>> after the push temp, but the non-inlined  #blah:  is an extra send
>> between which could be interrupted and prevent the #ensure from being
>> executed.
>>
>> >
>> > Else as I said at the beginning of the mail, a non local return can
>> > imply a
>> > process switch if that was your question.
>>
>> Yes, but a process switch is okay after the send of #ensure:
>> just not before.
>>
>> thx. cheers -ben
>>
>> >
>> >
>> >
>> >
>> >
>> >
>> >
>> > On Mon, Jun 6, 2016 at 2:46 PM, Ben Coman <[hidden email]> wrote:
>> >>
>> >> I understand generally that an inlined  #ifTrue  is atomic.  For
>> >> example, here after the primitive returns, no interruption can occur
>> >> before the #ensure is invoked...
>> >>     self primitiveWaitAcquire ifTrue:
>> >>            [mutuallyExclusiveBlock ensure: [self release]].
>> >>
>> >> But I want to know whether a non-local return could have an impact on
>> >> that atomicity. For example ...
>> >>     self primitiveWaitAcquire ifTrue:
>> >>            [ ^ mutuallyExclusiveBlock ensure: [self release]]
>> >>
>> >> cheers -ben
>> >>
>> >
>>
>

Reply | Threaded
Open this post in threaded view
|

Re: atomicity of non-local returns

Clément Béra


On Tue, Jun 7, 2016 at 1:55 AM, Ben Coman <[hidden email]> wrote:
On Tue, Jun 7, 2016 at 2:39 AM, Clément Bera <[hidden email]> wrote:
> Yeah anyway the non local return happens after the ensure: send, so nothing
> to worry about.
>
> In your analysis, you need to see that the #returnTop bytecode means NLR in
> a block but method return in a method. Even if it looks the same, in one
> case the #returnTop belongs to a block closure bytecode, hence is a NLR,
> whereas in the othercase, it belongs to the method bytecode, hence it's a
> method return.
>
> See StackInterpreter>>commonReturn, this code:
>
> "If this is a method simply return to the  sender/caller."
> (self iframeIsBlockActivation: localFP) ifFalse:
> [^self commonCallerReturn].
>

Ahhh, got it.  Quite a subtle semantic when the source code looks much the same.


> test if the return top is a method return or block NLR. I don't know why
> Eliot decided to share the bytecode between method return and block NLR, but
> there are 3 returns in Smalltalk, method return, block return and block NLR
> and they're all quite different.
>
> About your example, the normal closure bytecode creates the closure, pushes
> it on top of the stack, then skips over the closure bytecode (jump forward).
> About the jump instructions generated by ifTrue:ifFalse:, the jump forward
> instruction has no interrupt point, while the conditional jump have an
> interrupt point only in the case of a must be boolean. Does this make sense
> ?

Just to confirm in my own words...
the jumpFalse:
* sent to a boolean has no interrupt point
* sent to a non-boolean has an interrupt point

yes.
 

In Squeak there's indentation in bytecode printing to ease control flow
> understanding, that may help you. You are using a Squeak image for VM
> simulation and Pharo image to implement the primitives and experiment with
> the primitives, aren't you ?

The purpose of the primitives is for Pharo, but right this moment I'm
using Squeak for both the simulator and the image being simulated (my
changeset only needed a few changes to split Context back to its
earlier components).  While learning the simulator it was the path of
least resistance using the supplied scripts to build the reader image,
and I hadn't swapped to simulating a Pharo image yet.

cheers -ben

>
> Cheers
>
> On Mon, Jun 6, 2016 at 6:34 PM, Ben Coman <[hidden email]> wrote:
>>
>> On Mon, Jun 6, 2016 at 9:34 PM, Clément Bera <[hidden email]>
>> wrote:
>> > Hi Ben,
>> >
>> > Firstly be careful because atomic usually means that the thread cannot
>> > be
>> > interrupted by a concurrent native thread, so an atomic operation has to
>> > be
>> > guaranteed at processor level.
>> >
>> > Assuming that by atomic you mean that the operation's process cannot be
>> > preempted by another green thread, yes there is big problems with non
>> > local
>> > returns.
>>
>> Yes, I meant at the image level, run by the single-thread VM.
>>
>> >
>> > Two main issues:
>> > - NLR can trigger unwind blocks, which can switch to another process.
>> > - NLR can fail, triggering the cannotReturn: call-back, which can switch
>> > to
>> > another process.
>>
>> These would presumably could only occur after the #ensure: had been
>> invoked?  .
>>
>> >
>> > I understand generally that an inlined  #ifTrue  is atomic.  For
>> > example, here after the primitive returns, no interruption can occur
>> > before the #ensure is invoked...
>> >     self primitiveWaitAcquire ifTrue:
>> >            [mutuallyExclusiveBlock ensure: [self release]].
>> >
>> > The exact answer is that #ifTrue: can be interrupted if self
>> > primitiveWaitAcquire is not a boolean, and can't be interrupted if it's
>> > a
>> > boolean. It's nice to mark in a comment if part of the method's code has
>> > not
>> > to be interrupted to avoid issues years later.
>>
>> Good tip.
>>
>> >
>> > But I want to know whether a non-local return could have an impact on
>> > that atomicity. For example ...
>> >     self primitiveWaitAcquire ifTrue:
>> >            [ ^ mutuallyExclusiveBlock ensure: [self release]]
>> >
>> > In your code there is no non local return because ifTrue: is inlined by
>> > the
>> > Bytecode compiler.
>>
>> I'm not clear on your meaning.  If there were additional code
>> following those two lines, then they would not be executed, so it
>> would still be a NLR ?  I took my understanding from [1].  And in the
>> bytecode below it seems #returnTop is associated with the NLRs - so
>> the NLR is still there(?), but having looked at the bytecode I can now
>> see it has no impact, I think either with or without inlining makes no
>> difference wrt NLR.
>>
>>
>> [1]
>> https://silversmalltalk.wordpress.com/2011/02/02/implementing-smalltalks-non-local-returns-in-javascript/
>>
>> > Try to compile it and look at the bytecode you will see.
>>
>> Good idea. So I share a little analysis...
>>
>>
>> The bytecode for these two lines is identical,
>>     a blah: [ [ self foo ] ensure: [  self bar  ] ]
>>     a blah: [ ^ [ self foo ] ensure: [ self bar ] ]
>> except for bytecode 49, respectively...
>>      "49 <7D> blockReturn"
>>      "49 <7C> returnTop"
>>
>> "29 <10> pushTemp: 0"
>> "30 <8F 00 00 10> closureNumCopied: 0 numArgs: 0 bytes 34 to 49"
>> "34 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 38 to 40"
>> "38 <70> self"
>> "39 <D0> send: foo"
>> "40 <7D> blockReturn"
>> "41 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 45 to 47"
>> "45 <70> self"
>> "46 <D1> send: bar"
>> "47 <7D> blockReturn"
>> "48 <E2> send: ensure:"
>> "49 <7C> returnTop"
>> "50 <E3> send: blah:"
>> "51 <87> pop"
>> "52 <78> returnSelf"
>>
>>
>> Changing #blah: to #ifTrue:  these two lines
>>      a ifTrue: [ [ self foo ] ensure: [ self bar ] ]
>>      a ifTrue: [ ^ [ self foo ] ensure: [ self bar ] ]
>> are the same except for bytecode 49, respectively...
>>      "47 <87>  pop"
>>      "47 <7C> returnTop"
>>
>> "29 <10> pushTemp: 0"
>> "30 <AC 10> jumpFalse: 48"
>> "32 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 36 to 38"
>> "36 <70> self"
>> "37 <D0> send: foo"
>> "38 <7D> blockReturn"
>> "39 <8F 00 00 03> closureNumCopied: 0 numArgs: 0 bytes 43 to 45"
>> "43 <70> self"
>> "44 <D1> send: bar"
>> "45 <7D> blockReturn"
>> "46 <E2> send: ensure:"
>> "47 <87> pop"
>> "48 <78> returnSelf"
>>
>> And between #blah: and #ifTrue: the difference is
>> removal of...
>>     "30 <8F 00 00 10> closureNumCopied: 0 numArgs: 0 bytes 34 to 49"
>>     "49 <7D> blockReturn"
>>     "50 <E3> send: blah:"
>> and replaced them with...
>>     "30 <AC 10> jumpFalse: 48"
>>
>> It took me a while to guess how to follow the execution path.   I had
>> assumed it would just go top to bottom, but that didn't make sense for
>> foo to be executed before blah.  Can you confirm.. I guess the trick
>> (for #blah) is...
>>
>> 1. At 29, push the temp
>> 2. skip the closureNumCopied bytes 34 to 49,
>> 3. At 50, send #blah.
>> 4. next is bytecode 34 skips bytecodes 38 to 40,
>> 5. and bytecode 41 skips bytecodes 45 to 47
>> 6. At 48 send #ensure:.
>>
>> versus   #ifTrue:  bytecocde...
>>
>> 1. At 29, push the temp
>> 2. skip closureNumCopied bytes  36 to 38"
>> 3. skip closureNumCopied: bytes 43 to 45"
>> 4. At 46, send #ensure:
>>
>> So its clear (IIUC) that with  #IfTrue:  the #ensure is the first send:
>> after the push temp, but the non-inlined  #blah:  is an extra send
>> between which could be interrupted and prevent the #ensure from being
>> executed.
>>
>> >
>> > Else as I said at the beginning of the mail, a non local return can
>> > imply a
>> > process switch if that was your question.
>>
>> Yes, but a process switch is okay after the send of #ensure:
>> just not before.
>>
>> thx. cheers -ben
>>
>> >
>> >
>> >
>> >
>> >
>> >
>> >
>> > On Mon, Jun 6, 2016 at 2:46 PM, Ben Coman <[hidden email]> wrote:
>> >>
>> >> I understand generally that an inlined  #ifTrue  is atomic.  For
>> >> example, here after the primitive returns, no interruption can occur
>> >> before the #ensure is invoked...
>> >>     self primitiveWaitAcquire ifTrue:
>> >>            [mutuallyExclusiveBlock ensure: [self release]].
>> >>
>> >> But I want to know whether a non-local return could have an impact on
>> >> that atomicity. For example ...
>> >>     self primitiveWaitAcquire ifTrue:
>> >>            [ ^ mutuallyExclusiveBlock ensure: [self release]]
>> >>
>> >> cheers -ben
>> >>
>> >
>>
>