[Vm-dev] context change versus primitive failure return value (was: FFI exception failure support on Win64 (Win32 also?))

Ben Coman btc at openinworld.com
Thu Aug 30 07:05:03 UTC 2018


On Wed, 29 Aug 2018 at 11:08, Eliot Miranda <eliot.miranda at gmail.com> wrote:

>
> Well, the first thing to say is that this is a magnificent diagram; thank
> you.
>

Your welcome.  Actually I tried to avoid doing it because I've got some
other work to go on with ;)
but I got sucked in :).   Its a side effect of how I *need* to approach
understanding complex problems.
My eyes were glazing over getting lost with the tree of senders and
creating such a chart makes me pay attention to details.
btw, I created it with cross platform https://www.yworks.com/products/yed
(although I should be using Roassal to ensure completeness, doing it
manually provides time to aborb details)



> The problem is that the VM, and hence the diagram, is much more complex
> than the blue book specification, essentially because the VM is a highly
> optimized interpreter, whereas the specification is bare bones.  So I would
> ask you, and anyone else who wants to understand a Smalltalk-80 VM (a VM
> that provides Context objects for method activations, rather than a
> Smalltalk that uses a more conventional stack model) to read the Blue Book
> Specification:
>  http://www.mirandabanda.org/bluebook/bluebook_chapter28.html
> <http://www.mirandabanda.org/bluebook/bluebook_chapter28.html> carefully
> and fully. This is the last section of Smalltalk-80: The Language and its
> Implementation, by Adele Goldberg and David Robson.  The specification is
> well-written and clear and once digested serves as essential reference for
> understanding a more complex production VM.
>

I've been meaning to do this for a while, so took the opportunity just now
to read chapters 28 & 29.
I'm split my insights from that into a few posts.


> Now to your question: "I'm not at all clear about
> how internalExecuteNewMethod selects
> between internalQuickPrimitiveResponse and slowPrimitiveResponse, and what
> is the difference between them?".
>
> First, in a system that notionally allocates a context object to hold
> every activation, leaf routines are extremely expensive if all they do is
> answer a constant or an instance variable.  Dan Ingall's optimization is to
> avoid activations by providing a set of quick primitives that answer an
> instance variable whose slot index is from 0 to 255, or self, nil, true,
> false, -1, 0, 1 & 2.  The self, nil, true, false, -1, 0, 1 & 2 constants
> are derived from a static frequency analysis of literals and variable
> references in Smalltalk code and are echoed in the original bytecode set,
> byte codes 112-119 bering 01110iii Push (receiver, true, false, nil, -1, 0,
> 1, 2) [iii].  internalQuickPrimitiveResponse handles precisely these
> primitives, and these primitives only.  These primitives have the property
> that they can never fail, so they are invoked along a path that does not
> reset the flag used to identify primitive failure, nor test it.
>

I see BlueBook p605 & p620 code these primitives like this...

BlueBook Interpreter >> executeNewMethod    "no separate quick/slow
primitive handling here"
    self primitiveResponse
        ifFalse: [self activateNewMethod]

BlueBook Interpreter >> primitiveResponse
    | flagValue thisReceiver offset |
    primitiveIndex = 0
        ifTrue: [     "quick primitives"
            flagValue := self flagValueOf: newMethod.
            flagValue = 5 ifTrue: [self quickReturnSelf.   ^true].
            flagValue = 6 ifTrue: [self quickInstanceLoad. ^true].  "Quick
return inst vars"
            ^false]
        ifFalse: [    "slow primitives"
    self initPrimitive.
            self dispatchPrimitives.
            ^ self success]

BlueBook Interpreter >> quicklnstanceLoad  "Quick return inst vars, push
instance variable whose slot index is from 0 to 255"
    | thisReceiver fieldIndex |
    thisReceiver := self popStack.
    fieldlndex := self fieldlndexOf: newMethod.
    self push: (memory fetchPointer: fieldlndex ofObject: thisReceiver)

So it looks like {nil, true, false, -1, 0, 1 & 2} were not handled.
Although I see them handled by StackInterpreter.
I noted that StackInterpreter dispensed with "flagValue" and just used
primitive number to identify individual quick variables.

StackInterpreter >> externalQuickPrimitiveResponse
"Invoke a quick primitive.
Called under the assumption that primFunctionPtr has been preloaded"
| localPrimIndex |
self assert: self isPrimitiveFunctionPointerAnIndex.
localPrimIndex := self cCoerceSimple: primitiveFunctionPointer to: #sqInt.
self assert: (localPrimIndex > 255 and: [localPrimIndex < 520]).
"Quick return inst vars"
localPrimIndex >= 264 ifTrue:
[self pop: 1 thenPush: (objectMemory fetchPointer: localPrimIndex - 264
ofObject: self stackTop).
^true].
"Quick return constants"
localPrimIndex = 256 ifTrue: [^true "return self"].
localPrimIndex = 257 ifTrue: [self pop: 1 thenPush: objectMemory
trueObject. ^true].
localPrimIndex = 258 ifTrue: [self pop: 1 thenPush: objectMemory
falseObject. ^true].
localPrimIndex = 259 ifTrue: [self pop: 1 thenPush: objectMemory nilObject.
^true].
self pop: 1 thenPush: (objectMemory integerObjectOf: localPrimIndex - 261).
^true

And distinguishing between quick/slow primitives moved out to
executeNewMethod...

StackInterpreter >> executeNewMethod
"Execute newMethod - either primitiveFunctionPointer must be set directly
(i.e. from primitiveExecuteMethod et al), or it would have been set probing
the method cache (i.e. primitivePerform et al)."
primitiveFunctionPointer ~= 0 ifTrue:
[self isPrimitiveFunctionPointerAnIndex ifTrue:
[self externalQuickPrimitiveResponse.
^nil].
self slowPrimitiveResponse.
self successful ifTrue: [^nil]].
"if not primitive, or primitive failed, activate the method"
self activateNewMethod


Having soaked all that up, I've now better identified my difficulty
understanding
"how executeNewMethod distinguishes between quick and slow primitives"

I was looking at isPrimitiveFunctionPointerAnIndex (called from
internalExecuteMethod)
seeing it only checked   ```primitiveFunctionPointer <= 520```   to
identify quick primitives,
while quick primitives were between 256 to 520.  i.e. not checking
```primitiveFunctionPointer >=256```

StackInterpreter class >> initializePrimitiveTable
"Quick Push Const Methods primitiveIndex=0"
(256 nil) "primitivePushSelf"
(257 nil) "primitivePushTrue"
(258 nil) "primitivePushFalse"
(259 nil) "primitivePushNil"
(260 nil) "primitivePushMinusOne"
(261 nil) "primitivePushZero"
(262 nil) "primitivePushOne"
(263 nil) "primitivePushTwo"
"Quick Push Inst Var Methods"
(264 519 nil) "primitiveLoadInstVar"

But I now I realise that I didn't properly read/understand the comments
marked *** here...

StackInterpreter >> isPrimitiveFunctionPointerAnIndex
"We save slots in the method cache by using the primitiveFunctionPointer
to hold  ***either*** a function pointer or the index of a quick primitive.
         ***Since quick primitive indices are small they can't be confused
with function addresses*** ."
^(self cCoerce: primitiveFunctionPointer to: #'usqIntptr_t') <=
MaxQuickPrimitiveIndex

I haven't dug into where primitiveFunctionPointer is set, but I now get the
general idea.
cheers -ben
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.squeakfoundation.org/pipermail/vm-dev/attachments/20180830/b56a441a/attachment-0001.html>


More information about the Vm-dev mailing list