-
Notifications
You must be signed in to change notification settings - Fork 90
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
[RFC007] Bytecode interpreter #2045
base: master
Are you sure you want to change the base?
Conversation
Bencher Report
Click to view all benchmark results
|
Some parts might need refinement, but I think it's in a good shape for a first round of reviews. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some random comments.
The following notes on the memory representation applies to the native code | ||
backend's representation. I'm not sure how closures are represented in the Zinc | ||
Abstract Machine. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure it ought to be called Zinc anymore, but anyway: I'm pretty sure that the representation of values (including closures) is the same in native and bytecode. It must be so at least to some degree for the sake of the FFI, where Ocaml values can be manipulated.
no argument (the tag byte then doesn't store the actual contructor's tag but has | ||
the same value than for a boxed `int`). For a variant with parameters, the tag |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not quite it. A constructor without argument is represented as an unboxed integer value. It doesn't point to a block, so there is no tag involved. Constructors with arguments are pointers, and point to a structure as above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I don't know where I got this idea
Despite not being advertised, Haskell has an interpreter as well, which is used | ||
mostly for the GHCi REPL. This section describes what we know of the actual the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An Template Haskell.
code), such that thunk access is uniform: it's an unconditional jump to the | ||
corresponding code. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, when pointing to an info table, you actually point directly to the code pointer. The metadata in the info table is accessed backwards by subtracting from the pointer. This way entering a thunk is an indirect jump, which many processors support in a single instruction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right; I think it's mentioned in the STG paper that this was difficult to do with the approach proposed there going through ANSI C, but easy to do with a custom native code generation backend, which I suppose is what GHC does today.
The STG paper argues that this uniform thunk representation (with "self-handled | ||
update") simplifies the compilation process and gives room for some specific | ||
optimizations (vectored return for pattern matching, for example) that should be | ||
beneficial to Haskell programs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's worth noting that since the STG paper was written, things have become a little more complex. GHC uses some pointer tagging to mark if the thunk is already forced in the form of one of the 3 (on 32 bits) or 7 (on 64 bits) constructors of the data type. So pattern-matching will check these bits before entering the closure. For efficiency.
environment in the case of closures). As each constructor usage potentially | ||
generates very similar code, GHC is smart enough to share common constructors | ||
instead of generating them again and again (typically the one for an empty | ||
list). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An interesting difference between GHC's memory representation and Ocaml's is the way unboxed and boxed values are distinguished. This is only a concern for the GC, so maybe it doesn't matter too much for this discussion.
- In Ocaml, if a value, viewed as an unsigned int, is odd, then it's unboxed (and the GC doesn't follow it), when it's even, it's a pointer to a boxed value. This way it's always evident, when looking at a value whether it's a pointer or not. The cost is that integers lose one bit, and that it becomes impossible to unbox floats in most cases.
- In Haskell, pointers and non-pointer values are indistinguishable. So the GC asks the info table which of the fields are pointers. In practice, this is just a number, and all the non-pointer fields are stored first, and all the pointer fields after (or maybe the other way around, I don't remember).
machine. Those machines still have a number of differences on how they handle | ||
fundamental operations such as function application. We take inspiration from | ||
the | ||
[call-by-push-value](https://www.cs.bham.ac.uk/~pbl/papers/thesisqmwphd.pdf)(CBPV) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😊
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's funny because I clearly remember you telling me something like 4 years ago that if Nickel had a VM it should probably be a CPBV one. At that time I knew CPBV rather well but probably didn't have enough fresh VM or native compiler knowledge to really connect the two or really understand what it meant, I might have said something like "ah, ok", but it stuck as an (EDIT: un)evaluated thunk in the back of my head, so to speak.
Well, it took me a bit of time but I can now finally make sense of it 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I said that, I don't know. But I think my broader point was that I was thinking, at the time, of metadata (doc, default, …) as being attached to thunks. And CBPV helps us understand what's going on, because of thunking being an explicit operation in the language, so you can change what's going on there.
That being said, I always think of any evaluation model in terms of CBPV/polarised system L. Even when I don't present it that way: it's almost certainly a translation of the CPBV I have in my head. And I'm keen to embrace it in abstract machines as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Speaking of, you can think of system L as representing an abstract machine with a structured stack. Implemented directly, it's not as cache friendly as an actual stack machine. But maybe it's a middle ground to consider, as you don't have to come up with a complex linearised continuation representation for pattern matching, in particular.
On the other hand, maybe it's too tree-like for this proposal. I don't know, I haven't thought about it. I just said the name, and this idea popped up in response. Do whatever you want to do with it.
Although the name is a bit pompous, the goal of this RFC is mostly to be a working document for designing a more compact and efficient run-time representation for Nickel expressions.
While this is something that won't be user-facing (at least in a direct way), and thus can be changed later without breaking backward-compatibility, I think the technical scope of this effort is such that I find it better to discuss it formally here before going for a first implementation.