Re: guile 3 update, halloween edition
Hi :) On Sat 16 Nov 2019 16:26, Ludovic Courtès writes: > Andy Wingo skribis: > >> On Fri 15 Nov 2019 10:03, Ludovic Courtès writes: >> >>> I guess we could add a specific ‘’ exception or similar, >>> which would allow us to improve error reporting (that can come later, of >>> course.) > > What I meant is that type errors are “special” enough to deserve their > own type more specific than the catch-all ‘’ (just > like there’s already a separate ‘’, for instance.) Agreed! > Speaking of which, it seems that ‘set-guile-exception-converter!’ is > currently private, but I wonder if the goal was to make it public (it > seems to be unused)? It was private also in the exception conversion work that Mark did, FWIW; it just moved over as-is. Honestly I think that now that exceptions are "primary" we should probably move in the opposite direction: instead of adding more converters from key+args to exception objects, we should encourage exception throwers to switch from "throw" to "raise-exception", and allow library authors to define converters in the other way from exception object to the equivalent arguments for "catch". So I think exposing set-guile-exception-converter! might be the wrong thing at this point. Dunno tho. > For instance, C bindings that currently call ‘throw’ could provide > additional “exception converters” for the benefit of Scheme users > who’d rather use structured exceptions. (That would also give less of > an incentive to provide a C API for all of this.) This is a good point! FWIW Regarding C and migration, I have the impression that probably 90% of exception throwers in C use the helpers from error.h (scm_wrong_num_args and so on), which we can change transparently. A remaining 5% might use scm_error_scm, for which a registry might make sense, and 5% use scm_throw directly. These are just guesses tho. >>> 4. Is ‘’ actually used? Is the goal to make it continuable? >>> That sounds great. >> >> Any exception can be raised in a continuable way. Whether a raise is >> continuable or not depends on the value of the #:continuable? keyword to >> raise-exception. I think that's the intention of but I don't >> really have instincts about how it might be used. Guile defines it >> because it's in R6RS, but how it will be used is an open question :) > > I suppose the intent is to effectively allow users to implement the UI > stuff as a sort of co-routine to support separation of concerns: you > just raise a ‘’ that some other code displays in its preferred > way (console message, popup window, whatever) and eventually calls your > continuation. > > That’s something I’ve been wanting for some time, because right now > we’re able to separate out the UI concern for exception display, but not > for warnings. > > However, it seems that the handler passed to ‘with-exception-handler’ > does not receive the continuation, so is it the case that currently > handlers cannot resume exceptions? (Again not a showstopper IMO but > rather another wishlist item :-)). The handler runs within the continuation of "raise-continuable": (with-exception-handler (lambda (exn) (+ exn 30)) (lambda () (+ 2 (raise-exception 10 #:continuable? #t => 42 However I'm not sure this facility is what you want. Like for example there's lots of false-if-exception / catch #t out there; that's equivalent to: (define-syntax-rule (false-if-exception expr) (let/ec k (with-exception-handler (lambda (exn) (k #f)) (lambda () expr So the exception handler there would intervene and get a first crack at the warning, messing up your intent. To me warnings are like logging, and logging is notoriously difficult to standardize :) If it were me I would make a mechanism for warnings that had a with-warning-handler and I would make sure to raise all warnings via a separate raise-warning procedure or something, independent of exceptions. But that's just me :) Andy
Re: guile 3 update, halloween edition
Hi Andy! Andy Wingo skribis: > On Fri 15 Nov 2019 10:03, Ludovic Courtès writes: > >> 0. Do I get it right that ‘throw’ and ‘catch’ are not “deprecated” in >> the sense of (ice-9 deprecated) and are instead simply not the >> “preferred” exception mechanism? > > Correct. I think we could envision deprecating them in some future but > not within the next couple years at least. Alright, sounds good. >> I guess we could add a specific ‘’ exception or similar, >> which would allow us to improve error reporting (that can come later, of >> course.) What I meant is that type errors are “special” enough to deserve their own type more specific than the catch-all ‘’ (just like there’s already a separate ‘’, for instance.) > Yes. So right now Guile is in a bit of a transitional state -- it still > signals 99.9% of errors via `throw'. Probably we want to change to have > structured exceptions for almost all of these. To preserve > compatibility we would probably need to mix in an > to all of these exceptions, or otherwise > augment `exception-kind' and `exception-args' to synthesize these values > when appropriate. Yes, I agree. Speaking of which, it seems that ‘set-guile-exception-converter!’ is currently private, but I wonder if the goal was to make it public (it seems to be unused)? If it were public, I imagine that users could take advantage of it to support both exception styles, just like Guile does. For instance, C bindings that currently call ‘throw’ could provide additional “exception converters” for the benefit of Scheme users who’d rather use structured exceptions. (That would also give less of an incentive to provide a C API for all of this.) WDYT? >> Guix has ‘’ error conditions, which I’ve found useful when >> combined with other error conditions in cases where location info from >> the stack isn’t useful: >> >> https://git.savannah.gnu.org/cgit/guix.git/tree/guix/utils.scm#n832 >> >> I wonder if (ice-9 exceptions) should provide something like that. > > Neat :) Yes sure. I think, any exception type can be added to (ice-9 > exceptions) -- there's little "name cost" like there is in boot-9. > Which reminds me, I want to make boot-9 do a (resolve-module '(ice-9 > exceptions)) so that the more capable make-exception-from-throw always > gets installed. > >> 2. What are you thoughts regarding exposing structured exceptions to C? >> I’ve always been frustrated by ‘system-error’ :-). Guix has a hack to >> augment ‘system-error’ with information about the offending file name: >> >> https://git.savannah.gnu.org/cgit/guix.git/tree/guix/ui.scm#n520 >> >> If the POSIX bindings would emit a structured ‘’ record, >> that’d be pretty cool. > > I don't know :) Right now raise-exception is marked SCM_INTERNAL. > Probably it should be public. There is no public C API for any of this > new functionality, as it stands; a TODO. > > Regarding exception objects, there are two questions: one, how to create > exceptions of specific kinds; I suspect scm_make_system_error () would > be fine, and probably you want scm_make_exception to be able to mix > various exceptions. Second question, do we want to expose accessors > too? It can be a lot of API surface and I am a bit wary of it. But, > perhaps it is the right thing. I do not know. Yeah, it might be that having C stick to ‘throw’ + allowing for user-defined exception converters would be enough. >> 4. Is ‘’ actually used? Is the goal to make it continuable? >> That sounds great. > > Any exception can be raised in a continuable way. Whether a raise is > continuable or not depends on the value of the #:continuable? keyword to > raise-exception. I think that's the intention of but I don't > really have instincts about how it might be used. Guile defines it > because it's in R6RS, but how it will be used is an open question :) I suppose the intent is to effectively allow users to implement the UI stuff as a sort of co-routine to support separation of concerns: you just raise a ‘’ that some other code displays in its preferred way (console message, popup window, whatever) and eventually calls your continuation. That’s something I’ve been wanting for some time, because right now we’re able to separate out the UI concern for exception display, but not for warnings. However, it seems that the handler passed to ‘with-exception-handler’ does not receive the continuation, so is it the case that currently handlers cannot resume exceptions? (Again not a showstopper IMO but rather another wishlist item :-)). Thank you! Ludo’.
Re: guile 3 update, halloween edition
Hey thanks for the review :) On Fri 15 Nov 2019 10:03, Ludovic Courtès writes: > 0. Do I get it right that ‘throw’ and ‘catch’ are not “deprecated” in > the sense of (ice-9 deprecated) and are instead simply not the > “preferred” exception mechanism? Correct. I think we could envision deprecating them in some future but not within the next couple years at least. > 1. I see things like: > > +(define (make-condition type . field+value) > + "Return a new condition of type TYPE with fields initialized as specified > +by FIELD+VALUE, a sequence of field names (symbols) and values." > + (unless (exception-type? type) > +(scm-error 'wrong-type-arg "make-condition" "Not a condition type: ~S" > + (list type) #f)) > > and: > > + (unless (symbol? key) > +(throw 'wrong-type-arg "throw" "Wrong type argument in position ~a: > ~a" > + (list 1 key) (list key))) > > I guess we could add a specific ‘’ exception or similar, > which would allow us to improve error reporting (that can come later, of > course.) Yes. So right now Guile is in a bit of a transitional state -- it still signals 99.9% of errors via `throw'. Probably we want to change to have structured exceptions for almost all of these. To preserve compatibility we would probably need to mix in an to all of these exceptions, or otherwise augment `exception-kind' and `exception-args' to synthesize these values when appropriate. > Guix has ‘’ error conditions, which I’ve found useful when > combined with other error conditions in cases where location info from > the stack isn’t useful: > > https://git.savannah.gnu.org/cgit/guix.git/tree/guix/utils.scm#n832 > > I wonder if (ice-9 exceptions) should provide something like that. Neat :) Yes sure. I think, any exception type can be added to (ice-9 exceptions) -- there's little "name cost" like there is in boot-9. Which reminds me, I want to make boot-9 do a (resolve-module '(ice-9 exceptions)) so that the more capable make-exception-from-throw always gets installed. > 2. What are you thoughts regarding exposing structured exceptions to C? > I’ve always been frustrated by ‘system-error’ :-). Guix has a hack to > augment ‘system-error’ with information about the offending file name: > > https://git.savannah.gnu.org/cgit/guix.git/tree/guix/ui.scm#n520 > > If the POSIX bindings would emit a structured ‘’ record, > that’d be pretty cool. I don't know :) Right now raise-exception is marked SCM_INTERNAL. Probably it should be public. There is no public C API for any of this new functionality, as it stands; a TODO. Regarding exception objects, there are two questions: one, how to create exceptions of specific kinds; I suspect scm_make_system_error () would be fine, and probably you want scm_make_exception to be able to mix various exceptions. Second question, do we want to expose accessors too? It can be a lot of API surface and I am a bit wary of it. But, perhaps it is the right thing. I do not know. > 3. I wonder if we could take advantage of the new ‘’ exception > to start i18n of error messages. It might be as simple as telling > xgettext to recognize ‘make-exception-with-message’ as a keyword, though > currently there are few calls to ‘make-exception-with-message’ followed > by a literal. Eventually this will get called by `error', I think; Guile's R7RS layer in wip-r7rs defines `error' as being: (define (error message . irritants) (raise-exception (let ((exn (make-exception-with-message message))) (if (null? irritants) exn (make-exception exn (make-exception-with-irritants irritants)) But yes this is definitely something to think about it. > 4. Is ‘’ actually used? Is the goal to make it continuable? > That sounds great. Any exception can be raised in a continuable way. Whether a raise is continuable or not depends on the value of the #:continuable? keyword to raise-exception. I think that's the intention of but I don't really have instincts about how it might be used. Guile defines it because it's in R6RS, but how it will be used is an open question :) > Bah, you give us a present and I reply with an additional wishlist. > ;-) :) I hope that the exceptions work can serve as a foundation for further incremental, compatible improvement. Cheers, Andy
Re: guile 3 update, halloween edition
Hello Andy & all! Thanks for the great summary. I’ve taken a look at ‘wip-exceptions’, which is also remarkably easy to follow because all the changes are incremental and follow the path you explained in your message; thanks a lot for making it this clear! I’ve very much support this change, I always found the key+args convention to be poor compared to structured error condition objects. The changes in ‘wip-exceptions’ all make sense to me; some random comments below. 0. Do I get it right that ‘throw’ and ‘catch’ are not “deprecated” in the sense of (ice-9 deprecated) and are instead simply not the “preferred” exception mechanism? At least, that’s how I would view it :-), because ‘throw’ cannot (yet) disappear from C code, and because it’s a migration that could take more than two stable series to really complete. 1. I see things like: +(define (make-condition type . field+value) + "Return a new condition of type TYPE with fields initialized as specified +by FIELD+VALUE, a sequence of field names (symbols) and values." + (unless (exception-type? type) +(scm-error 'wrong-type-arg "make-condition" "Not a condition type: ~S" + (list type) #f)) and: + (unless (symbol? key) +(throw 'wrong-type-arg "throw" "Wrong type argument in position ~a: ~a" + (list 1 key) (list key))) I guess we could add a specific ‘’ exception or similar, which would allow us to improve error reporting (that can come later, of course.) Guix has ‘’ error conditions, which I’ve found useful when combined with other error conditions in cases where location info from the stack isn’t useful: https://git.savannah.gnu.org/cgit/guix.git/tree/guix/utils.scm#n832 I wonder if (ice-9 exceptions) should provide something like that. 2. What are you thoughts regarding exposing structured exceptions to C? I’ve always been frustrated by ‘system-error’ :-). Guix has a hack to augment ‘system-error’ with information about the offending file name: https://git.savannah.gnu.org/cgit/guix.git/tree/guix/ui.scm#n520 If the POSIX bindings would emit a structured ‘’ record, that’d be pretty cool. 3. I wonder if we could take advantage of the new ‘’ exception to start i18n of error messages. It might be as simple as telling xgettext to recognize ‘make-exception-with-message’ as a keyword, though currently there are few calls to ‘make-exception-with-message’ followed by a literal. 4. Is ‘’ actually used? Is the goal to make it continuable? That sounds great. Bah, you give us a present and I reply with an additional wishlist. ;-) Anyway, ‘wip-exceptions’ looks great to me as it is, so I’m all for merging it to ‘master’. Thanks a lot! Ludo’.
Re: guile 3 update, halloween edition
>> For the record, the bijection between R6RS conditions and Guile's throw >> arguments was my work, not Julian's. > > An honest mistake on my part. My sincere apologies! Thank you, Andy, I appreciate this. Thanks also for asking for input on the mailing list. I'm heavily overloaded at the moment, but I will try to respond to the other points in your email soon. It seems like a good approach, anyway. Best, Mark
Re: guile 3 update, halloween edition
On Sat 02 Nov 2019 06:20, Mark H Weaver writes: > Andy Wingo writes: > >> So, now the pending task is to somehow get a condition/exception >> hierarchy into Guile core. I will try to mostly push things off to side >> modules but it won't always be possible. There will be bijections >> between a Guile's "throw" arguments and structured exceptions, mostly >> inspired with what Julian did in the R6RS layer already. > > For the record, the bijection between R6RS conditions and Guile's throw > arguments was my work, not Julian's. An honest mistake on my part. My sincere apologies! Warm regards, Andy
Re: guile 3 update, halloween edition
Andy Wingo writes: > So, now the pending task is to somehow get a condition/exception > hierarchy into Guile core. I will try to mostly push things off to side > modules but it won't always be possible. There will be bijections > between a Guile's "throw" arguments and structured exceptions, mostly > inspired with what Julian did in the R6RS layer already. For the record, the bijection between R6RS conditions and Guile's throw arguments was my work, not Julian's. See: https://git.savannah.gnu.org/cgit/guile.git/commit/?id=02500d44775a77e46febfd47a0dab8233b0c99d0 Prior to that commit, there was no representation for Guile's "throw" arguments in terms of R6RS conditions, and moreover the R6RS exception guards were incapable of catching anything other than R6RS conditions. That terrible bug was reported by Göran Weinholt: https://bugs.gnu.org/14922 I fixed that bug, and announced the resulting work on guile-devel: https://lists.gnu.org/archive/html/guile-devel/2013-08/msg4.html Whatever you may think of me, please don't write me out of Guile's history. Thanks, Mark
Re: guile 3 update, halloween edition
On Thu, 31 Oct 2019 17:20:37 +0100 Andy Wingo wrote: > Greets :) > > On Thu 31 Oct 2019 01:01, Chris Vine writes: > > > "Condition" is a strange word for describing structured error objects, > > I agree. However, I think it would be quite confusing to describe > > error objects as exceptions. "Error object" or "error condition object" > > seems a reasonable alternative if the bare word "condition" is thought > > to be inappropriate. > > I'm very sympathetic to this argument -- an exception seems like a > thing-in-motion, not a thing-at-rest. But perhaps it's just the effect > of habit, setting up expectations about what good names are. (After > all, plenty of people seem happy with the term "condition"!) > > Perhaps there is a middle ground of sorts: maybe the manual can > comprehensively describe what R6RS refers to as conditions using the > term "exception objects". WDYT? I think "exception objects" would be fine. More broadly, I view an exception as something which makes the current thread of execution follow an exceptional path (say, implemented by some kind of continuation object), used generally but not exclusively to indicate that an error has occurred. An R6RS or SRFI-35 condition object on the other hand is a structured error information service, intended to be a thing (but not the only thing) which might be propagated as the payload of the exception, and which you can conveniently match on. Chris
Re: guile 3 update, halloween edition
Op wo 30 okt. 2019 21:55 schreef Andy Wingo : > > > Thoughts welcome! Also: should these structured error objects be named > exceptions or conditions? SRFI-35, R6RS, and R7RS say "conditions", but > racket and my heart say "exceptions"; wdyt? > To my experience they are different things. An exception breaks execution until caught/handled whereas conditions don't necessarily break execution, and are of more informational nature. What is described are exceptions (flow outside of the expected flow) yet thee should still be a condition based solution, perhaps using continuation mechanisms. My 2 cents Sjoerd > >
Re: guile 3 update, halloween edition
Greets :) On Thu 31 Oct 2019 01:01, Chris Vine writes: > "Condition" is a strange word for describing structured error objects, > I agree. However, I think it would be quite confusing to describe > error objects as exceptions. "Error object" or "error condition object" > seems a reasonable alternative if the bare word "condition" is thought > to be inappropriate. I'm very sympathetic to this argument -- an exception seems like a thing-in-motion, not a thing-at-rest. But perhaps it's just the effect of habit, setting up expectations about what good names are. (After all, plenty of people seem happy with the term "condition"!) Perhaps there is a middle ground of sorts: maybe the manual can comprehensively describe what R6RS refers to as conditions using the term "exception objects". WDYT? Andy
Re: guile 3 update, halloween edition
Hey :) On Thu 31 Oct 2019 15:17, Mikael Djurfeldt writes: > How does the record subtyping relate to GOOPS? I do realize that there > are issues related to keeping bootstrapping lean, but shouldn't record > types and classes share mechanisms? They share the struct layer. Records are simple: their fields are laid out in order, are all unboxed, and can be treated as a simple kind of nominal product type. `match' takes advantage of this. GOOPS is more flexible: it can have different slot allocations, unboxed slots, multiple inheritance, and so on. Because of multiple inheritance its accessors have to do dynamic dispatch; whereas for records, if supertype A allocates slot X to index I, all subtypes will have that slot in the same place. A check whether a record is an instance of A is a simple O(1) check, rather than the CPL search of GOOPS. Exceptions have a kind of multiple inheritance, but it's more about object composition than typing. You can make a compound condition from a heterogeneous collection of other conditions. While you could implement compound conditions with subtyping, it's more straightforward to use composition. I think the current status is close to the sweet spot but your thoughts are welcome :) I would dearly like to be able to subtype records in GOOPS, but having looked at it a few times I haven't found quite the right way to do it. Cheers, Andy
Re: guile 3 update, halloween edition
Hello Andy, > ... > Thoughts welcome! Also: should these structured error objects be > named exceptions or conditions? SRFI-35, R6RS, and R7RS say > "conditions", but racket and my heart say "exceptions"; wdyt? I personally prefer "exceptions" over "conditions", though I did read and understand Chris answer ... Now, slightly of topic, but since/while you are working on (ice-9 boot), exceptions ... please allow me this gentle ping and invite you to (re)read my answer to the following 'bug report' (1), which is a 'claim' in favor of, in 3.0, to have the repl, errors and backtraces printers to truncate by default, then, to make things so that it would be 'dead easy' to toggle those to full print 'on demand' ... up to one could then bind the toggle proc(s) to an FN key ... (1) https://debbugs.gnu.org/cgi/bugreport.cgi?bug=36677#23 To 'quote myself': Imo, no matter how easy it would be/is to change, the default should be to enable truncated printing for the repl, erros and backtraces, then indeed we should have 'dead easy' 'toggle' mechanism for those 'extremely rare' guilers/situations who/that requires/want (occasionally in my experience, I sometimes do to of course) full print ... I was recently reading an email (or was it on irc, I don't remember) fom Rekado, who wrote guile-studio, which is great news, and an attempt make guile's newbie first experience(s) quite a lot more friendly ... With the above claim (and a 'dead easy' toggle mechanism ...), I would feel quite a lot more 'relax' to (try to) recommend guile-cv ... and together with guile-studio ... it then really can be said 'try it, you'll even have fun ...' David pgpWhaXJuiTLI.pgp Description: OpenPGP digital signature
Re: guile 3 update, halloween edition
Saying this without having looked at your code and also without currently promising to do any work: How does the record subtyping relate to GOOPS? I do realize that there are issues related to keeping bootstrapping lean, but shouldn't record types and classes share mechanisms? Best regards, Mikael On Wed, Oct 30, 2019 at 9:55 PM Andy Wingo wrote: > Hey folks! > > I wanted to send out an update on Guile 3. Do take a look at > https://git.savannah.gnu.org/cgit/guile.git/tree/NEWS to see where we've > come; basically the JIT is done, and we're ready to release soonish. > > However! Here begins a long chain of yak-shaving: > > I wanted good benchmarks. Generally up to now, we haven't really been > doing good incremental benchmarks. Ideally we could see some benchmark > results historically, on every commit, and against other Scheme > implementations. To a degree it's been possible with > https://ecraven.github.io/r7rs-benchmarks/, but those benchmarks have a > few problems: > > (1) They use unsafe optimizations on e.g. Chez and Gambit > (2) They are infrequently run > (3) They rely on R7RS, adding their own little compat layer for Guile, > which isn't optimal. > > Now, regarding (3), probably Guile should just have its own R7RS layer. > And it should be easier to enable R6RS too. So I added an --r6rs > option, and started importing some R7RS code from Göran Weinholt > (thanks!), with the idea of adding --r7rs. That way we can just > benchmark the different implementations, just passing --r7rs or whatever > to get the behavior we want. We can reduce the set of other scheme > implementations to just the high-performance ones: Gambit, Larceny, > Chez, and Racket. > > However! R7RS, like R6RS and like SRFI-35/SRFI-34, and also like > Racket, specifies an error-handling system in terms of "raise" and > "with-exception-handler". Guile uses "throw" and "catch". There is a > pretty good compatibility layer in Guile's R6RS exceptions/conditions > code, but it's not shared by SRFI-35/SRFI-34, and unless we built R7RS > in terms of R6RS -- something I prefer not to do; these things should be > layered on Guile core directly -- we'd have to duplicate the mechanism. > > Which, of course, is a bit trash. And when you come to think of it, > throw/catch/with-throw-handler is also a bit trash. It is too hard to > handle exceptions in Guile; the addition of `print-exception' a few > years back improved things, but still, making any sense out of the > "args" corresponding to a "key" is a mess. > > All this made me think -- Guile should probably switch to > raise/with-exception-handler and structured exceptions. (Or conditions, > or whatever we choose to call them. I find the "condition" name a bit > weird but maybe that's just a personal problem.) Racket does this too, > for what it's worth, though they have their own historical baggage. > > But, we need to maintain compatibility with throw/catch, because that's > not going anywhere any time soon (if ever). So I hacked a bit and > eventually came up with a decent implementation of throw/catch on top of > raise/with-exception-handler, and I think it's compatible in all the > weird ways that it needs to be. > > But! Now we have bootstrapping problems; how to get the implementation > in boot-9? Exceptions in SRFI-35, R6RS, R7RS, and Racket are these > hierarchical things: they form a DAG of subtypes. But core records in > Guile aren't subtypeable, so what to do? > > Well, my thinking was that we needed to sedimentarily deposit down into > Guile core those commonalities between the different record > implementations in Guile: SRFI-35 conditions, R6RS records, and SRFI-9 > records. So core now has the notion of field mutability on the record > layer (as opposed to the struct layer), a notion of subtyping, a notion > of extensibility, and so on. This is all now in the manual and will be > in NEWS. > > With that, we now have just one implementation of records!!! I am very > pleased about this. Now you can use core record introspection > facilities on any record in Guile. Cool. This also helped untangle > some knots in the R6RS inter-module graph. > > So, now the pending task is to somehow get a condition/exception > hierarchy into Guile core. I will try to mostly push things off to side > modules but it won't always be possible. There will be bijections > between a Guile's "throw" arguments and structured exceptions, mostly > inspired with what Julian did in the R6RS layer already. > > Thoughts welcome! Also: should these structured error objects be named > exceptions or conditions? SRFI-35, R6RS, and R7RS say "conditions", but > racket and my heart say "exceptions"; wdyt? > > Cheers, > > Andy > >
Re: guile 3 update, halloween edition
On Wed, Oct 30, 2019 at 4:55 PM Andy Wingo wrote: > > Thoughts welcome! Also: should these structured error objects be named > exceptions or conditions? SRFI-35, R6RS, and R7RS say "conditions", but > racket and my heart say "exceptions"; wdyt? I think "exceptions" is a better name for the reasons others have already stated. Excited for 3.0! - Dave
Re: guile 3 update, halloween edition
Hi Andy! Thanks for all the work! On Thu, Oct 31, 2019 at 4:55 AM Andy Wingo wrote: > Hey folks! > > I wanted to send out an update on Guile 3. Do take a look at > https://git.savannah.gnu.org/cgit/guile.git/tree/NEWS to see where we've > come; basically the JIT is done, and we're ready to release soonish. > Guile powered by JIT was confirmed to increase at least 20% performance for Artanis. And this is a rough result of the early-bird version of Guile-2.9. I haven't tried the latest. I'm looking forward to an official release of Guile-3 so that I can manage to support the version detection correctly. It was 2.9 but should detect as 3.0, IIRC. > But! Now we have bootstrapping problems; how to get the implementation > in boot-9? Exceptions in SRFI-35, R6RS, R7RS, and Racket are these > hierarchical things: they form a DAG of subtypes. But core records in > Guile aren't subtypeable, so what to do? > Are you talking about Guile specific record-type? Personally, I've gradually reduced my usage of Guile records. I think R6RS records are better for me. I didn't know the Guile bootstrapping requires Guile specific record-type. So I don't know better advice. > There will be bijections > between a Guile's "throw" arguments and structured exceptions, mostly > inspired with what Julian did in the R6RS layer already. > That's cool! > Thoughts welcome! Also: should these structured error objects be named > exceptions or conditions? SRFI-35, R6RS, and R7RS say "conditions", but > racket and my heart say "exceptions"; wdyt? > I never say "condition" to describe an exception. I always say "exception". Most other languages use "exception" too. The term "condition" sounds like conditional branching. Best regards.
Re: guile 3 update, halloween edition
On Wed, 30 Oct 2019 21:13:49 +0100 Andy Wingo wrote: > Also: should these structured error objects be named > exceptions or conditions? SRFI-35, R6RS, and R7RS say "conditions", but > racket and my heart say "exceptions"; wdyt? R6RS and R7RS speak of raising an exception, and handling the exception in an exception handler, and racket uses similar language. According to R6RS "when an exception is raised, an object is provided that describes the nature of the exceptional situation. The report uses the condition system described in library section 7.2 to describe exceptional situations, classifying them by condition types". However, condition objects are optional when an exception is raised - you can just as well use a symbol, or a symbol/string pair, for simple cases. "Condition" is a strange word for describing structured error objects, I agree. However, I think it would be quite confusing to describe error objects as exceptions. "Error object" or "error condition object" seems a reasonable alternative if the bare word "condition" is thought to be inappropriate.
Re: guile 3 update, halloween edition
Hi Andy, Wonderful update. I'll only comment on one thing. Andy Wingo writes: > Thoughts welcome! Also: should these structured error objects be named > exceptions or conditions? SRFI-35, R6RS, and R7RS say "conditions", but > racket and my heart say "exceptions"; wdyt? Exceptions, since it's what everyone uses, so "conditions" would make Guile be an exception to the use of "exceptions".
guile 3 update, halloween edition
Hey folks! I wanted to send out an update on Guile 3. Do take a look at https://git.savannah.gnu.org/cgit/guile.git/tree/NEWS to see where we've come; basically the JIT is done, and we're ready to release soonish. However! Here begins a long chain of yak-shaving: I wanted good benchmarks. Generally up to now, we haven't really been doing good incremental benchmarks. Ideally we could see some benchmark results historically, on every commit, and against other Scheme implementations. To a degree it's been possible with https://ecraven.github.io/r7rs-benchmarks/, but those benchmarks have a few problems: (1) They use unsafe optimizations on e.g. Chez and Gambit (2) They are infrequently run (3) They rely on R7RS, adding their own little compat layer for Guile, which isn't optimal. Now, regarding (3), probably Guile should just have its own R7RS layer. And it should be easier to enable R6RS too. So I added an --r6rs option, and started importing some R7RS code from Göran Weinholt (thanks!), with the idea of adding --r7rs. That way we can just benchmark the different implementations, just passing --r7rs or whatever to get the behavior we want. We can reduce the set of other scheme implementations to just the high-performance ones: Gambit, Larceny, Chez, and Racket. However! R7RS, like R6RS and like SRFI-35/SRFI-34, and also like Racket, specifies an error-handling system in terms of "raise" and "with-exception-handler". Guile uses "throw" and "catch". There is a pretty good compatibility layer in Guile's R6RS exceptions/conditions code, but it's not shared by SRFI-35/SRFI-34, and unless we built R7RS in terms of R6RS -- something I prefer not to do; these things should be layered on Guile core directly -- we'd have to duplicate the mechanism. Which, of course, is a bit trash. And when you come to think of it, throw/catch/with-throw-handler is also a bit trash. It is too hard to handle exceptions in Guile; the addition of `print-exception' a few years back improved things, but still, making any sense out of the "args" corresponding to a "key" is a mess. All this made me think -- Guile should probably switch to raise/with-exception-handler and structured exceptions. (Or conditions, or whatever we choose to call them. I find the "condition" name a bit weird but maybe that's just a personal problem.) Racket does this too, for what it's worth, though they have their own historical baggage. But, we need to maintain compatibility with throw/catch, because that's not going anywhere any time soon (if ever). So I hacked a bit and eventually came up with a decent implementation of throw/catch on top of raise/with-exception-handler, and I think it's compatible in all the weird ways that it needs to be. But! Now we have bootstrapping problems; how to get the implementation in boot-9? Exceptions in SRFI-35, R6RS, R7RS, and Racket are these hierarchical things: they form a DAG of subtypes. But core records in Guile aren't subtypeable, so what to do? Well, my thinking was that we needed to sedimentarily deposit down into Guile core those commonalities between the different record implementations in Guile: SRFI-35 conditions, R6RS records, and SRFI-9 records. So core now has the notion of field mutability on the record layer (as opposed to the struct layer), a notion of subtyping, a notion of extensibility, and so on. This is all now in the manual and will be in NEWS. With that, we now have just one implementation of records!!! I am very pleased about this. Now you can use core record introspection facilities on any record in Guile. Cool. This also helped untangle some knots in the R6RS inter-module graph. So, now the pending task is to somehow get a condition/exception hierarchy into Guile core. I will try to mostly push things off to side modules but it won't always be possible. There will be bijections between a Guile's "throw" arguments and structured exceptions, mostly inspired with what Julian did in the R6RS layer already. Thoughts welcome! Also: should these structured error objects be named exceptions or conditions? SRFI-35, R6RS, and R7RS say "conditions", but racket and my heart say "exceptions"; wdyt? Cheers, Andy
Re: guile 3 update, september edition
Hello, Andy Wingo skribis: > On Mon 17 Sep 2018 11:35, l...@gnu.org (Ludovic Courtès) writes: > >>> The threshold at which Guile will automatically JIT-compile is set from >>> the GUILE_JIT_THRESHOLD environment variable. By default it is 5. >>> If you set it to -1, you disable the JIT. If you set it to 0, *all* >>> code will be JIT-compiled. The test suite passes at >>> GUILE_JIT_THRESHOLD=0, indicating that all features in Guile are >>> supported by the JIT. Set the GUILE_JIT_LOG environment variable to 1 >>> or 2 to see JIT progress. >> >> Just to be clear, does GUILE_JIT_THRESHOLD represents the number of >> times a given instruction pointer is hit? > > No. It is an abstract "hotness" counter associated with a function's > code. (I say "function's code" because many closures can share the same > code and thus the same counter. It's not in the scm_tc7_program object > because some procedures don't have these.) > > All counters start at 0 when Guile starts. A function's counters > increment by 30 when a function is called, currently, and 2 on every > loop back-edge. I have not attempted to tweak these values yet. OK, I see. Exciting times! Ludo’.
Re: guile 3 update, september edition
Greets :) On Mon 17 Sep 2018 11:35, l...@gnu.org (Ludovic Courtès) writes: >> The threshold at which Guile will automatically JIT-compile is set from >> the GUILE_JIT_THRESHOLD environment variable. By default it is 5. >> If you set it to -1, you disable the JIT. If you set it to 0, *all* >> code will be JIT-compiled. The test suite passes at >> GUILE_JIT_THRESHOLD=0, indicating that all features in Guile are >> supported by the JIT. Set the GUILE_JIT_LOG environment variable to 1 >> or 2 to see JIT progress. > > Just to be clear, does GUILE_JIT_THRESHOLD represents the number of > times a given instruction pointer is hit? No. It is an abstract "hotness" counter associated with a function's code. (I say "function's code" because many closures can share the same code and thus the same counter. It's not in the scm_tc7_program object because some procedures don't have these.) All counters start at 0 when Guile starts. A function's counters increment by 30 when a function is called, currently, and 2 on every loop back-edge. I have not attempted to tweak these values yet. >> Using GNU Lightning has been useful but in the long term I don't think >> it's the library that we need, for a few reasons: > > [...] > > It might be that the lightning 1.x branch would be a better fit (it was > exactly as you describe.) I think that’s what Racket was (is?) using. Could be! I will have a look. Cheers, Andy
Re: guile 3 update, september edition
Le lun. 17 sept. 2018 à 10:26, Andy Wingo a écrit : > Hi! > > This is an update on progress towards Guile 3. In our last update, we > saw the first bits of generated code: > > https://lists.gnu.org/archive/html/guile-devel/2018-08/msg5.html > > Since then, the JIT is now feature-complete. It can JIT-compile *all* > code in Guile, including delimited continuations, dynamic-wind, all > that. It runs automatically, in response to a function being called a > lot. It can also tier up from within hot loops. > This looks very good! When the merge will be done, maybe it will be time to move guile-next to master in guix? WDYT? Keep it steady!
Re: guile 3 update, september edition
Hello! Andy Wingo skribis: > This is an update on progress towards Guile 3. In our last update, we > saw the first bits of generated code: > > https://lists.gnu.org/archive/html/guile-devel/2018-08/msg5.html > > Since then, the JIT is now feature-complete. It can JIT-compile *all* > code in Guile, including delimited continuations, dynamic-wind, all > that. It runs automatically, in response to a function being called a > lot. It can also tier up from within hot loops. Woohoo! It’s awesome that JIT can already handle all Guile code and run all the test suite. To me that means it can be merged into ‘master’. > The threshold at which Guile will automatically JIT-compile is set from > the GUILE_JIT_THRESHOLD environment variable. By default it is 5. > If you set it to -1, you disable the JIT. If you set it to 0, *all* > code will be JIT-compiled. The test suite passes at > GUILE_JIT_THRESHOLD=0, indicating that all features in Guile are > supported by the JIT. Set the GUILE_JIT_LOG environment variable to 1 > or 2 to see JIT progress. Just to be clear, does GUILE_JIT_THRESHOLD represents the number of times a given instruction pointer is hit? > For debugging (single-stepping, tracing, breakpoints), Guile will fall > back to the bytecode interpreter (the VM), for the thread that has > debugging enabled. Once debugging is no longer enabled (no more hooks > active), that thread can return to JIT-compiled code. Cool. > Using GNU Lightning has been useful but in the long term I don't think > it's the library that we need, for a few reasons: [...] It might be that the lightning 1.x branch would be a better fit (it was exactly as you describe.) I think that’s what Racket was (is?) using. > Meaning that "eval" in Guile 3 is somewhere around 80% faster than in > Guile 2.2 -- because "eval" is now JIT-compiled. Very cool. > Incidentally, as a comparison, Guile 2.0 (whose "eval" is slower for > various reasons) takes 70s real time for the same benchmark. Guile 1.8, > whose eval was written in C, takes 4.536 seconds real time. It's still > a little faster than Guile 3's eval-in-Scheme, but it's close and we're > catching up :) It’s an insightful comparison; soon we can say it’s “as fast as hand-optimized C” (and more readable, too :-)). > I have also tested with ecraven's r7rs-benchmarks and we make a nice > jump past the 2.2 results; but not yet at Racket or Chez levels yet. I > think we need to tighten up our emitted code. There's another 2x of > performance that we should be able to get with incremental improvements. > For the last bit we will need global register allocation though, I > think. Looking forward to reading ecraven’s updated benchmark results. Thank you for the awesomeness! Ludo’.
guile 3 update, september edition
Hi! This is an update on progress towards Guile 3. In our last update, we saw the first bits of generated code: https://lists.gnu.org/archive/html/guile-devel/2018-08/msg5.html Since then, the JIT is now feature-complete. It can JIT-compile *all* code in Guile, including delimited continuations, dynamic-wind, all that. It runs automatically, in response to a function being called a lot. It can also tier up from within hot loops. The threshold at which Guile will automatically JIT-compile is set from the GUILE_JIT_THRESHOLD environment variable. By default it is 5. If you set it to -1, you disable the JIT. If you set it to 0, *all* code will be JIT-compiled. The test suite passes at GUILE_JIT_THRESHOLD=0, indicating that all features in Guile are supported by the JIT. Set the GUILE_JIT_LOG environment variable to 1 or 2 to see JIT progress. For debugging (single-stepping, tracing, breakpoints), Guile will fall back to the bytecode interpreter (the VM), for the thread that has debugging enabled. Once debugging is no longer enabled (no more hooks active), that thread can return to JIT-compiled code. Right now the JIT-compiled code exactly replicates what the bytecode interpreter does: the same stack reads and writes, etc. There is some specialization when a bytecode has immediate operands of course. However the choice to do debugging via the bytecode interpreter -- effectively, to always have bytecode around -- will allow machine code (compiled either just-in-time or ahead-of-time) to do register allocation. JIT will probably do a simple block-local allocation. An AOT compiler is free to do something smarter. As far as I can tell, with the default setting of GUILE_JIT_THRESHOLD=5, JIT does not increase startup latency for any workload, and always increases throughput. More benchmarking is needed though. Using GNU Lightning has been useful but in the long term I don't think it's the library that we need, for a few reasons: * When Lightning does a JIT compilation, it builds a graph of operations, does some minor optimizations, and then emits code. But the graph phase takes time and memory. I think we just need a library that just emits code directly. That would lower the cost of JIT and allow us to lower the default GUILE_JIT_THRESHOLD. * The register allocation phase in Lightning exists essentially for calls. However we have a very restricted set of calls that we need to do, and can do the allocation by hand on each architecture. This (We don't use CPU call instructions for Scheme function calls because we use the VM stack. We might be able to revise this in the future but again Lightning is in the way). Doing it by hand would allow a few benefits: * Hand allocation would free up more temporary registers. Right now lightning reserves all registers used as part of the platform calling convention; they are unavailable to the JIT. * Sometimes when Lightning needs a temporary register, it can clobber one that we're using as part of an internal calling convention. I believe this is fixed for x86-64 but I can't be sure for other architectures! See commit 449ef7d9755b553cb0ad2629bca3bc42c5913e88. * We need to do our own register allocation; having Lightning also do it is a misfeature. * Sometimes we know that we can get better emitted code, but the lightning abstraction doesn't let us do it. We should allow ourselves to punch through that abstraction. The platform-specific Lightning files basically expose most of the API we need. We could consider incrementally punching through lightning.h to reach those files. Something to think about for the future. Finally, as far as performance goes -- we're generally somewhere around 80% faster than 2.2. Sometimes more, sometimes less, always faster though AFAIK. As an example, here's a simple fib.scm: $ cat /tmp/fib.scm (define (fib n) (if (< n 2) 1 (+ (fib (- n 1)) (fib (- n 2) Now let's use eval-in-scheme to print the 35th fibonacci number. For Guile 2.2: $ time /opt/guile-2.2/bin/guile -c \ '(begin (primitive-load "/tmp/fib.scm") (pk (fib 35)))' ;;; (14930352) real 0m9.610s user 0m10.547s sys 0m0.040s But with Guile from the lightning branch, we get: $ time /opt/guile/bin/guile -c \ '(begin (primitive-load "/tmp/fib.scm") (pk (fib 35)))' ;;; (14930352) real 0m5.299s user 0m6.167s sys 0m0.064s Meaning that "eval" in Guile 3 is somewhere around 80% faster than in Guile 2.2 -- because "eval" is now JIT-compiled. (Otherwise it's the same program.) This improves bootstrap times, though Guile 3's compiler will generally make more CPS nodes than Guile 2.2 for the same expression, which takes more time and memory, so the gain isn't earth-shattering. Incidentally, as a comparison, Guile 2.0 (whose
Re: Guile 3 update, August edition
On Mon, Aug 20, 2018 at 10:27 AM, Andy Wingo wrote: > In this particular example, the JITted code runs about 3x faster than > the interpreted code. The JIT doesn't do register allocation; not sure > precisely how to do that. A future topic. For the moment I want to > consolidate what we have and once it's all just magically working and > everybody's programs are faster, we release Guile 3. Echoing everyone else: This is very exciting! I can't wait to take it for a spin. Thank you! - Dave
Re: Guile 3 update, August edition
Le ven. 24 août 2018 à 14:19, Christopher Lemmer Webber < cweb...@dustycloud.org> a écrit : > Andy Wingo writes: > > > In this particular example, the JITted code runs about 3x faster than > > the interpreted code. The JIT doesn't do register allocation; not sure > > precisely how to do that. A future topic. For the moment I want to > > consolidate what we have and once it's all just magically working and > > everybody's programs are faster, we release Guile 3. > > This is great news! Very excited about Guile 3 over here! :) > > Same here! Thanks a lot for this updates. I abandonned my Chez scheme adventures somewhat thanks to those updates! Keep it steady and happy hacking!
Re: Guile 3 update, August edition
Andy Wingo writes: > In this particular example, the JITted code runs about 3x faster than > the interpreted code. The JIT doesn't do register allocation; not sure > precisely how to do that. A future topic. For the moment I want to > consolidate what we have and once it's all just magically working and > everybody's programs are faster, we release Guile 3. This is great news! Very excited about Guile 3 over here! :)
Guile 3 update, August edition
Hi! Last dispatch was here: https://lists.gnu.org/archive/html/guile-devel/2018-07/msg00037.html To recap, I merged in GNU lightning and added an extra machine-code return address to frames, but hadn't actually written the JIT yet. Since July, I made it so that all Guile bytecode function entry points start with an "instrument-entry" bytecode that holds a counter. The intention is that when the counter increments beyond a certain value, the function should be automatically JIT-compiled. Associated with the counter is a native-code pointer corresponding to the function. I also added "instrument-loop" bytecodes to all loops, to be able to tier up from within hot loops. With all of this done and some other bytecode tweaks, I was able to move on to the JIT compiler itself. I'm happy to say that I now have a first version. It's about 3500 lines of C, so a bit gnarly. It's architecture-independent, as it uses lightning, and there are lightning backends for about every architecture. Lightning seems OK. Not optimal, but OK, and an OK thing to use for now anyway. I did have to write special cases for 32-bit machines, as Guile's VM supports 64-bit arithmetic, and some-endian-specific code. I probably got some of that wrong; review is very welcome: https://git.savannah.gnu.org/cgit/guile.git/tree/libguile/jit.c?h=lightning If you have fixes and are a committer, please feel free to just commit them directly. If you aren't a committer yet and you spot some fixes, mail the list; you should definitely be a committer if you can do that :) I just got the JIT working today. For the time being, the interface is a public function, %jit-compile. Eventually I will remove this when I have more confidence, relying only on the automatic compilation triggered by function entry and loop iterations. As an example: $ cat foo.scm (use-modules (rnrs bytevectors)) (define (f32v-sum bv) (let lp ((n 0) (sum 0.0)) (if (< n (bytevector-length bv)) (lp (+ n 4) (+ sum (bytevector-ieee-single-native-ref bv n))) sum))) (define ones (make-f32vector #e1e7 1.0)) # The JIT currently doesn't emit hook code. $ meta/guile --no-debug scheme@(guile-user)> (load "foo.scm") scheme@(guile-user)> ,time (f32v-sum ones) $2 = 1.0e7 ;; 0.143017s real time, 0.142986s run time. 0.00s spent in GC. scheme@(guile-user)> (%jit-compile f32v-sum) scheme@(guile-user)> ,time (f32v-sum ones) $3 = 1.0e7 ;; 0.048514s real time, 0.048499s run time. 0.00s spent in GC. In this particular example, the JITted code runs about 3x faster than the interpreted code. The JIT doesn't do register allocation; not sure precisely how to do that. A future topic. For the moment I want to consolidate what we have and once it's all just magically working and everybody's programs are faster, we release Guile 3. Cheers, Andy
guile 3 update, july edition
Hi :) Just a brief update with Guile 3. Last one was here: https://lists.gnu.org/archive/html/guile-devel/2018-06/msg00026.html There is a now a "lightning" branch that has GNU lightning merged in and built statically into Guile. It causes about 1 MB of overhead in the -Og libguile-3.0.so, bringing it to 6.1 MB, or 1.36 MB stripped. Seems OK for now. By way of contrast, libguile-2.2.so is 5.65 MB when built with -Og, or 1.19 MB stripped. There's some scaffolding for making JIT code emitters for each instruction. But then I ran into a problem about how to intermingle JIT and interpreter returns on the stack. I was hoping to avoid having separate interpreter and JIT return addresses in a stack frame, to avoid adding overhead. That didn't work out: https://lists.gnu.org/archive/html/guile-devel/2018-07/msg00013.html So, I added a slot to the "overhead" part of stack frames. From frames.h: Stack frame layout -- | ... | +==+ <- fp + 3 = SCM_FRAME_PREVIOUS_SP (fp) | Dynamic link | +--+ | Virtual return address (vRA) | +--+ | Machine return address (mRA) | +==+ <- fp | Local 0 | +--+ | Local 1 | +--+ | ... | +--+ | Local N-1| \--/ <- sp The stack grows down. The calling convention is that a caller prepares a stack frame consisting of the saved FP, the saved virtual return addres, and the saved machine return address of the calling function, followed by the procedure and then the arguments to the call, in order. Thus in the beginning of a call, the procedure being called is in slot 0, the first argument is in slot 1, and the SP points to the last argument. The number of arguments, including the procedure, is thus FP - SP. That took a while. Anything that changes calling conventions is gnarly. While I was at it, I changed the return calling convention to expect return values from slot 0 instead of from slot 1, and made some other minor changes to instructions related to calls and returns. The next step will be to add an "enter-function" instruction or something to function entries. This instruction's only real purpose will be to increment a counter associated with the function. If the counter exceeds some threshold, JIT code will be emitted for the function and the function will tier up. If the enter-function instruction sees that the function already has JIT code (e.g. emitted from another thread), then it will tier up directly. Because enter-function is in the right place to run the apply hook for debugging, we'll probably move that functionality there, instead of being inline with the call instructions. The "enter-function" opcode will take an offset to writable data for the counter, allocated in the ELF image. This data will have the form: struct jit_data { void* mcode; uint32_t counter; uint32_t start; uint32_t end; } The mcode pointer indicates the JIT code, if any. It will probably need to be referenced atomically (maybe release/consume ordering?). The counter is the counter associated with this function; entering a function will increment it by some amount. The start and end elements indicate the bounds of the function, and are offsets into the vcode, relative to the jit_data struct. These are not writable. Loops will also have an instruction that increments the counter, possibly tiering up if needed. The whole function will share one "struct jit_data". I am currently thinking that we can make JIT-JIT function calls peek ahead in the vcode of the callee to find the callee JIT code, if any. I.e.: (if (has-tc7? callee %tc7-program) (let ((vcode (word-ref callee 1))) (if (= (logand (u32-ref vcode 0) #xff) %enter-function-opcode) (let ((mcode ((+ vcode (* (u32-ref vcode 1) 4) (if (zero? mcode) (jmp! mcode) (return!))) ;; return to interpreter (return!))) (return!)) It's a dependent memory load on the function-call hot path but it will predict really well. The upside of this is that there is just one mutable mcode pointer for a function, for all its closures in all threads. It also avoids reserving more space on the heap for another mcode word in program objects. Loops will tier up ("on-stack replacement") by jumping to an offset in the mcode corresponding to the vcode for the counter-incrementing instruction. The offset will be determined by running the JIT compiler for the function but without actually emitting the code and flushing icache; the compiler is run in a mode just to determine the
Re: guile 3 update, june 2018 edition
dsm...@roadrunner.com wrote: > Ok! now getting past the "make -j" issue, but I'm still getting a segfault. And now commit e6461cf1b2b63e3ec9a2867731742db552b61b71 has gotten past the segfault. Wooo! -Dale
Re: guile 3 update, june 2018 edition
Hi :) On Mon 02 Jul 2018 11:28, l...@gnu.org (Ludovic Courtès) writes: > Andy Wingo skribis: > >> My current plan is that the frame overhead will still be two slots: the >> saved previous FP, and the saved return address. Right now the return >> address is always a bytecode address. In the future it will be bytecode >> or native code. Guile will keep a runtime routine marking regions of >> native code so it can know if it needs to if an RA is bytecode or native >> code, for debugging reasons; but in most operation, Guile won't need to >> know. The interpreter will tier up to JIT code through an adapter frame >> that will do impedance matching over virtual<->physical addresses. To >> tier down to the interpreter (e.g. when JIT code calls interpreted >> code), the JIT will simply return to the interpreter, which will pick up >> state from the virtual IP, SP, and FP saved in the VM state. > > What will the “adapter frame” look like? Aah, sadly it won't work like this. Somehow I was thinking of an adapter frame on the C stack. However an adapter frame corresponds to a continuation, so it would have to have the life of a continuation, so it would have to be on the VM stack. I don't think I want adapter frames on the VM stack, so I have to scrap this. More below... >> We do walk the stack from Scheme sometimes, notably when making a >> backtrace. So, we'll make the runtime translate the JIT return >> addresses to virtual return addresses in the frame API. To Scheme, it >> will be as if all things were interpreted. > > Currently you can inspect the locals of a stack frame. Will that be > possible with frames corresponding to native code? (I suppose that’d be > difficult.) Yes, because native code manipulates the VM stack in exactly the same way as bytecode. Eventually we should do register allocation and avoid always writing values to the stack, but that is down the road. >> My current problem is knowing when a callee has JIT code. Say you're in >> JITted function F which calls G. Can you directly jump to G's native >> code, or is G not compiled yet and you need to use the interpreter? I >> haven't solved this yet. "Known calls" that use call-label and similar >> can of course eagerly ensure their callees are JIT-compiled, at >> compilation time. Unknown calls are the problem. I don't know whether >> to consider reserving another word in scm_tc7_program objects for JIT >> code. I have avoided JIT overhead elsewhere and would like to do so >> here as well! > > In the absence of a native code pointer in scm_tc7_program objects, how > will libguile find the native code for a given program? This is a good question and it was not clear to me when I wrote this! I think I have a solution now but it involves memory overhead. Oh well. Firstly, I propose to add a slot to stack frames. Stack frames will now store the saved FP, the virtual return address (vRA), and the machine return address IP (mRA). When in JIT code, a return will check if the mRA is nonzero, and if so jump to that mRA. Otherwise it will return from JIT, and the interpreter should continue. Likewise when doing a function return from the interpreter and the mRA is nonzero, the interpreter should return by entering JIT code to that address. When building an interpreter-only Guile (Guile without JIT) or an AOT-only Guile (doesn't exist currently), we could configure Guile to not reserve this extra stack word. However that would be a different ABI: a .go file built with interpreter-only Guile wouldn't work on Guile-with-JIT, because interpreter-only Guile would think stack frames only need two reserved words, whereas Guile-with-JIT would write three words. To avoid the complication, for 3.0 I think we should just use 3-word frames all the time. So, that's returns. Other kinds of non-local returns like abort-to-prompt, resuming delimited continuations, or calling undelimited continuations would work similarly: the continuation would additionally record an mRA, and resuming would jump there instead, if appropriate. Now, calls. One of the reasons that I wanted to avoid an extra program word was because scm_tc7_program doesn't exist in a one-to-one relationship with code. "Well-known" procedures get compiled by closure optimization to be always called via call-label or tail-call-label -- so some code doesn't have program objects. On the other hand, closures mean that some code has many program objects. So I thought about using side tables indexed by code; or inline "maybe-tier-up-here" instructions, which would reference a code pointer location, that if nonzero, would be the JIT code. However I see now that really we need to optimize for the JIT-to-JIT call case, as by definition that's going to be the hot case. Of course call-label from JIT can do an unconditional jmp. But calling a program object... how do we do this? This is complicated by code pages being read-only, so we don't have space to store a pointer in
Re: guile 3 update, june 2018 edition
Hello! Andy Wingo skribis: > The news is that the VM has been completely converted over to call out > to the Guile runtime through an "intrinsics" vtable. For some > intrinsics, the compiler will emit specialized call-intrinsic opcodes. > (There's one of these opcodes for each intrinsic function type.) For > others that are a bit more specialized, like the intrinsic used in > call-with-prompt, the VM calls out directly to the intrinsic. > > The upshot is that we're now ready to do JIT compilation. JIT-compiled > code will use the intrinsics vtable to embed references to runtime > routines. In some future, AOT-compiled code can keep the intrinsics > vtable in a register, and call indirectly through that register. Exciting! It sounds like a really good strategy because it means that the complex instructions don’t have to be implemented in lightning assembly by hand, which would be a pain. > My current plan is that the frame overhead will still be two slots: the > saved previous FP, and the saved return address. Right now the return > address is always a bytecode address. In the future it will be bytecode > or native code. Guile will keep a runtime routine marking regions of > native code so it can know if it needs to if an RA is bytecode or native > code, for debugging reasons; but in most operation, Guile won't need to > know. The interpreter will tier up to JIT code through an adapter frame > that will do impedance matching over virtual<->physical addresses. To > tier down to the interpreter (e.g. when JIT code calls interpreted > code), the JIT will simply return to the interpreter, which will pick up > state from the virtual IP, SP, and FP saved in the VM state. What will the “adapter frame” look like? > We do walk the stack from Scheme sometimes, notably when making a > backtrace. So, we'll make the runtime translate the JIT return > addresses to virtual return addresses in the frame API. To Scheme, it > will be as if all things were interpreted. Currently you can inspect the locals of a stack frame. Will that be possible with frames corresponding to native code? (I suppose that’d be difficult.) > My current problem is knowing when a callee has JIT code. Say you're in > JITted function F which calls G. Can you directly jump to G's native > code, or is G not compiled yet and you need to use the interpreter? I > haven't solved this yet. "Known calls" that use call-label and similar > can of course eagerly ensure their callees are JIT-compiled, at > compilation time. Unknown calls are the problem. I don't know whether > to consider reserving another word in scm_tc7_program objects for JIT > code. I have avoided JIT overhead elsewhere and would like to do so > here as well! In the absence of a native code pointer in scm_tc7_program objects, how will libguile find the native code for a given program? Thanks for sharing this plan! Good times ahead! Ludo’.
Re: guile 3 update, june 2018 edition
Ok! now getting past the "make -j" issue, but I'm still getting a segfault. Here is a backtrace from the core dump. Line 25: #25 0x7efeb518b09f in scm_error (key=0x563599bbb120, subr=subr@entry=0x0, message=message@entry=0x7efeb521c0cd "Unbound variable: ~S", args=0x563599f8f260, rest=rest@entry=0x4) at error.c:62 Looks kinda suspicious. Should subr be 0x0 there? Thread 4 (Thread 0x7efeb282c700 (LWP 10059)): #0 pthread_cond_wait@@GLIBC_2.3.2 () at ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185 #1 0x7efeb4401cc7 in GC_wait_marker () from /usr/lib/x86_64-linux-gnu/libgc.so.1 #2 0x7efeb43f85ca in GC_help_marker () from /usr/lib/x86_64-linux-gnu/libgc.so.1 #3 0x7efeb440033c in GC_mark_thread () from /usr/lib/x86_64-linux-gnu/libgc.so.1 #4 0x7efeb49f6494 in start_thread (arg=0x7efeb282c700) at pthread_create.c:333 #5 0x7efeb4738acf in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:97 Thread 3 (Thread 0x7efeb382e700 (LWP 10057)): #0 pthread_cond_wait@@GLIBC_2.3.2 () at ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185 #1 0x7efeb4401cc7 in GC_wait_marker () from /usr/lib/x86_64-linux-gnu/libgc.so.1 #2 0x7efeb43f85ca in GC_help_marker () from /usr/lib/x86_64-linux-gnu/libgc.so.1 #3 0x7efeb440033c in GC_mark_thread () from /usr/lib/x86_64-linux-gnu/libgc.so.1 #4 0x7efeb49f6494 in start_thread (arg=0x7efeb382e700) at pthread_create.c:333 #5 0x7efeb4738acf in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:97 Thread 2 (Thread 0x7efeb302d700 (LWP 10058)): #0 pthread_cond_wait@@GLIBC_2.3.2 () at ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_wait.S:185 #1 0x7efeb4401cc7 in GC_wait_marker () from /usr/lib/x86_64-linux-gnu/libgc.so.1 #2 0x7efeb43f85ca in GC_help_marker () from /usr/lib/x86_64-linux-gnu/libgc.so.1 #3 0x7efeb440033c in GC_mark_thread () from /usr/lib/x86_64-linux-gnu/libgc.so.1 #4 0x7efeb49f6494 in start_thread (arg=0x7efeb302d700) at pthread_create.c:333 #5 0x7efeb4738acf in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:97 Thread 1 (Thread 0x7efeb565d740 (LWP 10034)): #0 0x7efeb51afd26 in scm_maybe_resolve_module (name=name@entry=0x563599f8f140) at modules.c:195 #1 0x7efeb51b01bf in scm_public_variable (module_name=0x563599f8f140, name=0x563599da50e0) at modules.c:656 #2 0x7efeb518036a in init_print_frames_var_and_frame_to_stack_vector_var () at backtrace.c:103 #3 0x7efeb49fd739 in __pthread_once_slow (once_control=0x7efeb545d828 , init_routine=0x7efeb5180340 ) at pthread_once.c:116 #4 0x7efeb49fd7e5 in __GI___pthread_once (once_control=once_control@entry=0x7efeb545d828 , init_routine=init_routine@entry=0x7efeb5180340 ) at pthread_once.c:143 #5 0x7efeb51801b0 in display_backtrace_body (a=0x7ffe2b3b7ea0) at backtrace.c:218 #6 0x7efeb520040f in vm_regular_engine (thread=0x563599b14dc0) at vm-engine.c:610 #7 0x7efeb52046d3 in scm_call_n (proc=proc@entry=0x563599da5aa0, argv=argv@entry=0x0, nargs=nargs@entry=0) at vm.c:1440 #8 0x7efeb518cab9 in scm_call_0 (proc=proc@entry=0x563599da5aa0) at eval.c:489 #9 0x7efeb51f8cd6 in catch (tag=tag@entry=0x404, thunk=0x563599da5aa0, handler=0x563599da5940, pre_unwind_handler=0x4) at throw.c:144 #10 0x7efeb51f9015 in scm_catch_with_pre_unwind_handler (key=key@entry=0x404, thunk=, handler=, pre_unwind_handler=) at throw.c:262 #11 0x7efeb51f91cf in scm_c_catch (tag=tag@entry=0x404, body=body@entry=0x7efeb5180190 , body_data=body_data@entry=0x7ffe2b3b7ea0, handler=handler@entry=0x7efeb5180580 , handler_data=handler_data@entry=0x563599baf000, pre_unwind_handler=pre_unwind_handler@entry=0x0, pre_unwind_handler_data=0x0) at throw.c:387 #12 0x7efeb51f91de in scm_internal_catch (tag=tag@entry=0x404, body=body@entry=0x7efeb5180190 , body_data=body_data@entry=0x7ffe2b3b7ea0, handler=handler@entry=0x7efeb5180580 , handler_data=handler_data@entry=0x563599baf000) at throw.c:396 #13 0x7efeb5180185 in scm_display_backtrace_with_highlights (stack=stack@entry=0x563599da5b60, port=port@entry=0x563599baf000, first=first@entry=0x4, depth=depth@entry=0x4, highlights=highlights@entry=0x304) at backtrace.c:277 #14 0x7efeb51f8fec in handler_message (tag=tag@entry=0x563599bbb120, args=args@entry=0x563599c0cdb0, handler_data=) at throw.c:548 #15 0x7efeb51f93cb in scm_handle_by_message (handler_data=, tag=0x563599bbb120, args=0x563599c0cdb0) at throw.c:585 #16 0x7efeb51f94fe in default_exception_handler (args=0x563599c0cdb0, k=0x563599bbb120) at throw.c:174 #17 throw_without_pre_unwind (tag=0x563599bbb120, args=0x563599c0cdb0) at throw.c:248 #18 0x7efeb520040f in vm_regular_engine (thread=0x563599b14dc0) at vm-engine.c:610 #19 0x7efeb52046d3 in scm_call_n (proc=proc@entry=0x563599baf9c0, argv=, nargs=5) at vm.c:1440 ---Type to continue, or q to quit--- #20 0x7efeb518ce4b in scm_apply_0
Re: guile 3 update, june 2018 edition
Greetings Andy! Andy Wingo wrote: > Hi, > > Just wanted to give an update on Guile 3 developments. Last note was > here: > > https://lists.gnu.org/archive/html/guile-devel/2018-04/msg4.html > > The news is that the VM has been completely converted over to call out > to the Guile runtime through an "intrinsics" vtable. For some > intrinsics, the compiler will emit specialized call-intrinsic opcodes. > (There's one of these opcodes for each intrinsic function type.) For > others that are a bit more specialized, like the intrinsic used in > call-with-prompt, the VM calls out directly to the intrinsic. Very exciting! However, master is not building for me. :( git clean -dxf; ./autogen.sh && ./configure && make -j5 gives me SNARF atomic.x SNARF backtrace.x SNARF boolean.x In file included from atomic.c:29:0: extensions.h:26:30: fatal error: libguile/libpath.h: No such file or directory #include "libguile/libpath.h" ^ compilation terminated. Makefile:3893: recipe for target 'atomic.x' failed make[2]: *** [atomic.x] Error 1 Maybe some dependency tuning is needed? So. Building without -j : make clean; make gives gives a segfault when generating the docs SNARF regex-posix.doc GEN guile-procedures.texi Uncaught exception: Backtrace: /bin/bash: line 1: 13428 Broken pipe cat alist.doc array-handle.doc array-map.doc arrays.doc async.doc atomic.doc backtrace.doc boolean.doc bitvectors.doc bytevectors.doc chars.doc control.doc continuations.doc debug.doc deprecated.doc deprecation.doc dynl.doc dynwind.doc eq.doc error.doc eval.doc evalext.doc expand.doc extensions.doc fdes-finalizers.doc feature.doc filesys.doc fluids.doc foreign.doc fports.doc gc-malloc.doc gc.doc gettext.doc generalized-arrays.doc generalized-vectors.doc goops.doc gsubr.doc guardians.doc hash.doc hashtab.doc hooks.doc i18n.doc init.doc ioext.doc keywords.doc list.doc load.doc macros.doc mallocs.doc memoize.doc modules.doc numbers.doc objprop.doc options.doc pairs.doc ports.doc print.doc procprop.doc procs.doc promises.doc r6rs-ports.doc random.doc rdelim.doc read.doc rw.doc scmsigs.doc script.doc simpos.doc smob.doc sort.doc srcprop.doc srfi-1.doc srfi-4.doc srfi-13.doc srfi-14.doc srfi-60.doc stackchk.doc stacks.doc stime.doc strings.doc strorder.doc strports.doc struct.doc symbols.doc syntax.doc threads.doc throw.doc trees.doc unicode.doc uniform.doc values.doc variable.doc vectors.doc version.doc vports.doc weak-set.doc weak-table.doc weak-vector.doc dynl.doc posix.doc net_db.doc socket.doc regex-posix.doc 13429 Segmentation fault | GUILE_AUTO_COMPILE=0 ../meta/build-env guild snarf-check-and-output-texi > guile-procedures.texi Makefile:3910: recipe for target 'guile-procedures.texi' failed This is $ git describe v2.2.2-504-gb5dcdf2e2 And gcc is $ gcc --version gcc (Debian 6.3.0-18+deb9u1) 6.3.0 20170516 On an up to date Debian 9.4 system: $ uname -a Linux debmetrix 4.9.0-6-amd64 #1 SMP Debian 4.9.88-1+deb9u1 (2018-05-07) x86_64 GNU/Linux -Dale
guile 3 update, june 2018 edition
Hi, Just wanted to give an update on Guile 3 developments. Last note was here: https://lists.gnu.org/archive/html/guile-devel/2018-04/msg4.html The news is that the VM has been completely converted over to call out to the Guile runtime through an "intrinsics" vtable. For some intrinsics, the compiler will emit specialized call-intrinsic opcodes. (There's one of these opcodes for each intrinsic function type.) For others that are a bit more specialized, like the intrinsic used in call-with-prompt, the VM calls out directly to the intrinsic. The upshot is that we're now ready to do JIT compilation. JIT-compiled code will use the intrinsics vtable to embed references to runtime routines. In some future, AOT-compiled code can keep the intrinsics vtable in a register, and call indirectly through that register. My current plan is that the frame overhead will still be two slots: the saved previous FP, and the saved return address. Right now the return address is always a bytecode address. In the future it will be bytecode or native code. Guile will keep a runtime routine marking regions of native code so it can know if it needs to if an RA is bytecode or native code, for debugging reasons; but in most operation, Guile won't need to know. The interpreter will tier up to JIT code through an adapter frame that will do impedance matching over virtual<->physical addresses. To tier down to the interpreter (e.g. when JIT code calls interpreted code), the JIT will simply return to the interpreter, which will pick up state from the virtual IP, SP, and FP saved in the VM state. We do walk the stack from Scheme sometimes, notably when making a backtrace. So, we'll make the runtime translate the JIT return addresses to virtual return addresses in the frame API. To Scheme, it will be as if all things were interpreted. This strategy relies on the JIT being a simple code generator, not an optimizer -- the state of the stack whether JIT or interpreted is the same. We can consider relaxing this in the future. My current problem is knowing when a callee has JIT code. Say you're in JITted function F which calls G. Can you directly jump to G's native code, or is G not compiled yet and you need to use the interpreter? I haven't solved this yet. "Known calls" that use call-label and similar can of course eagerly ensure their callees are JIT-compiled, at compilation time. Unknown calls are the problem. I don't know whether to consider reserving another word in scm_tc7_program objects for JIT code. I have avoided JIT overhead elsewhere and would like to do so here as well! For actual JIT code generation, I think my current plan is to import a copy of GNU lightning into Guile's source, using git-subtree merges. Lightning is fine for our purposes as we only need code generation, not optimization, and it supports lots of architectures: ARM, MIPS, PPC, SPARC, x86 / x86-64, IA64, HPPA, AArch64, S390, and Alpha. Lightning will be built statically into libguile. This has the advantage that we always know the version being used, and we are able to extend lightning without waiting for distros to pick up a new version. Already we will need to extend it to support atomic ops. Subtree merges should allow us to pick up upstream improvements without too much pain. This strategy also allows us to drop lightning in the future if that's the right thing. Basically from the user POV it should be transparent. The whole thing will be behind an --enable-jit / --disable-jit configure option. When it is working we can consider enabling shared lightning usage. Happy hacking, Andy
Guile 3 update -- more instruction explosion, intrinsics
Hello all :) This mail is an update on Guile 3 progress. Some previous updates were here: https://lists.gnu.org/archive/html/guile-devel/2018-01/msg9.html https://lists.gnu.org/archive/html/guile-devel/2018-01/msg3.html I took a break for a while and picked up this work again a couple weeks ago. I think the reason I stopped was because I got to agonizing about how to call out to run-time routines from eventual native code. Like, you want to call out from native code to scm_string_set_x; or some more specialized routine that's part of the ABI but not the API, like an internal internal_string_set_x routine. How do you get the pointer to that code? How do you manage saving the VM state and restoring it? Can you have custom calling conventions? How do you preserve cross-process sharing of code pages? For a while I thought inline caches would be a kind of answer, but after looking at it for a while I think I am going to punt: https://wingolog.org/archives/2018/02/07/design-notes-on-inline-caches-in-guile Guile simply isn't all that polymorphic, and the set of runtime routines callable from native code compiled by Guile is bounded. Better instead to simply provide a vtable to native code that contains function pointers to anything that might need to be callable at run-time. So that's what I settled on. I call them "intrinsics" (I would call them "builtins" but that word is used for something else). I started moving over "macro-instructions" that can't be usefully broken apart, like string-set!, to be intrinsic calls. AOT-compiled native code will compile these to vtable calls ("call [reg + offset]"), where reg holds a pointer to the intrinsics and offset is a fixed offset. JIT-compiled native code can inline the intrinsic address of course. I also pushed ahead with instruction explosion for string-length, string-ref, the atomics, integer/char conversion, make-closure, and f64->scm. I made a bunch more instructions be intrinsics. The VM is thus now smaller and has fewer instructions whose implementations contain internal branches, which means we're getting closer to native code. There are still some more instructions to push to intrinsics. (Q: When should an instruction be an intrinsic rather than "exploded" (lowered to more primitive instructions and control flow)? A: When the optimizer is unlikely to be able to elide components of the exploded implementation.) Then the biggest remaining task is dealing with the call instructions, which are somewhat large still. Probably they need exploding. Once that's done I think we can look to implementing a simple template method JIT. If the performance of that beats 2.2 (as I think it should!), then it could be a good point to release 3.0 just like that. Long-term I think it makes sense to do AOT compilation. That also opens the door for self-hosted adaptive optimization. But, I don't know quite how to get there and keep our portability story. Do we keep bytecode forever? Only in some cases? I don't know. I certainly want to minimize the amount of C that we have to maintain :) Likewise in the medium term I think we should be actively moving library code from C to Scheme. With the move to intrinsics in the VM, the VM itself is relying less and less on libguile, making the whole system less coupled to libguile. For Guile to prosper in the next 10 years, we need to be able to retarget it, to WebAssembly and Racket-on-Chez and PyPy and Graal and a whole host of other things. The compiler is producing low-level, fairly portable output currently, which is appropriate to this goal, but we are limited by libguile. So let's be thinking about how to move much of the remaining 80KLOC of C over to Scheme. We won't be able to move all of it, but certainly we can move some of it. Happy hacking, Andy
Re: guile 3 update: instruction explosion for pairs, vectors
On Tue, Jan 16, 2018 at 4:55 PM, Andy Wingowrote: > Thinking more globally, there are some more issues -- one is that > ideally we need call-site specialization. A GF could be highly > polymorphic globally but monomorphic for any given call site. We need > away to specialize. > Yes, but I imagine that the gain of having polymorphic inline caches compared to just GFs will decrease the more that type dispatch can be eliminated from compiled method bodies (the monomorphic case will be replaced by a direct call of the IM (compiled method)). Then, of course, a polymorphic inline cache can perhaps be regarded as an anonymous GF such that there isn't really much difference. Dynamically typed data structures would be the remaining main source of type dispatch. > Secondly, it would be nice of course to have speculative optimization, > including speculative inlining and type specialization not only on GF > arguments but also arguments to regular calls, types of return values, > and so on. > Yes! I think that dispatch on the return values is interesting. What I'm now going to write is based on almost zero knowledge of compiler construction, and I still will have to learn the Guile compiler infrastructure (where is a good start?), so please bear with me. For the same reason, what I write might be completely obvious and well-known already. Imagine that we are compiling the body of a method and we arrive at an integer addition. At some step of compilation, there has been a conversion to CPS such that we (at some level) can regard the operation as: (+ a b k) where k is the continuation. This means that k will be called with the result of the addition: (k ) In a framework where essentially all procedures are GFs, also k, here, is a GF. This allows it to dispatch on its argument such that it can treat inums, bignums, floats and doubles differently. Note now that in such a framework, a GF might have only one method (M) but the instantiated/compiled methods (IMs) can still be many. If k above is called with an inum, there will be an IM of k which is specialized to inums. This means that the compiler later can choose operations relevant for inums inside k. The "exploded" (in a slightly different sense) code for + above might, in turn, contain a branch which handles the transition into bignums at "inum overflow". If now k above come to occur in a branch of the +-code, inlined in an outer function, where the argument of k is guaranteed to be an inum, then our GF dispatch elimination will replace k with with the k-IM for inums. So, the *only* branch remaining in the code will be the overflow check in +. (BTW, I wonder if this inlining/specialization to an outer function could in some sense rely on type dispatch on the continuation k?) > > Finally I wonder that if we had the latter, if it matters so much about > optimizing generic functions in any kind of specific way -- instead you > could just have a generic optimizer. > Yes. I guess I'm mostly using my GFs as a "hook" for my thoughts on this. The reason I do this is that I can imagine a reasonably simple implementation where (almost) everything is a GF. :) There, the GFs would, in some sense, work as data structures for the compiler/optimizer. > > Of course the speculative optimizer could work on traces instead of > methods, and in that case we'd get a lot of this stuff for free... but > that's a question for farther down the road. See > http://scheme2016.snow-fort.org/static/scheme16-paper3.pdf. > Yes, this is very nice and exciting work. :) Best regards, Mikael
guile 3 update: instruction explosion for bytevectors
Greets, FYI I wrote up a new update on the road to Guile 3; as I seem to be getting closer, it's in blog form: https://wingolog.org/archives/2018/01/17/instruction-explosion-in-guile Cheers, A
Re: guile 3 update: instruction explosion for pairs, vectors
On Tue 09 Jan 2018 15:30, Mikael Djurfeldtwrites: > Maybe this is all completely obvious to you, but I want to remind, > again, about the plans and ideas I had for GOOPS before I had to leave > it at its rather prototypical and unfinished state: > > As you recall, generic functions (GFs) then carried a cache (ideas > taken from PCL) with "instantiated methods" (IM; there is probably a > better term and I might have called them "compiled methods" or > "cmethods" before)---method code where the types of the arguments are > known since each instance come from an actual invocation of the > GF. Given a new invocation, the dispatch mechanism would just use > argument types to look up the correct IM. > > I then had the idea that since we know the types of the arguments of > the IM, a lot of the type dispatch could be eliminated within the IM > based on flow information---very much in line with what you are doing > here. If we now add one more piece, things get really exciting: > Wherever there is a call to other GFs within one IM and the types of > the arguments can be deduced at the point of the call, then the > polymorphic type dispatch of the GF can be eliminated and we can > replace the GF call with a direct call to the corresponding IM. I totally agree! You could call the compiler at runtime to produce a type-specialized method. There are some tricky things (how to embed a precompiled Tree-IL representation of the method into the reified .go file? Possible but needs work). Thinking more globally, there are some more issues -- one is that ideally we need call-site specialization. A GF could be highly polymorphic globally but monomorphic for any given call site. We need away to specialize. Secondly, it would be nice of course to have speculative optimization, including speculative inlining and type specialization not only on GF arguments but also arguments to regular calls, types of return values, and so on. Finally I wonder that if we had the latter, if it matters so much about optimizing generic functions in any kind of specific way -- instead you could just have a generic optimizer. Of course the speculative optimizer could work on traces instead of methods, and in that case we'd get a lot of this stuff for free... but that's a question for farther down the road. See http://scheme2016.snow-fort.org/static/scheme16-paper3.pdf. Cheers, and happy new year to you too! Andy
Re: guile 3 update: instruction explosion for pairs, vectors
Hi, Hi think this is a marvelous development and, for what it's worth, in the right direction. Many, many thanks! Maybe this is all completely obvious to you, but I want to remind, again, about the plans and ideas I had for GOOPS before I had to leave it at its rather prototypical and unfinished state: As you recall, generic functions (GFs) then carried a cache (ideas taken from PCL) with "instantiated methods" (IM; there is probably a better term and I might have called them "compiled methods" or "cmethods" before)---method code where the types of the arguments are known since each instance come from an actual invocation of the GF. Given a new invocation, the dispatch mechanism would just use argument types to look up the correct IM. I then had the idea that since we know the types of the arguments of the IM, a lot of the type dispatch could be eliminated within the IM based on flow information---very much in line with what you are doing here. If we now add one more piece, things get really exciting: Wherever there is a call to other GFs within one IM and the types of the arguments can be deduced at the point of the call, then the polymorphic type dispatch of the GF can be eliminated and we can replace the GF call with a direct call to the corresponding IM. Given now that most of the standard scheme functions can be regarded as polymorphic GFs, I then imagined that most of the type dispatch in a program could be eliminated. Actual execution would mostly be direct calls of IMs to IMs, something which the optimizer could continue to work on, especially if it all was represented as CPS. Given your work here, I think that something like this could now rather easily be implemented. That is, we re-introduce IMs, make them directly callable, and substitute IM calls for GF calls when compiling them. I gather that the code of IMs do not necessarily have to be hung onto GFs but could be handled by some separate manager/data structures. Happy new year! Mikael On Mon, Jan 8, 2018 at 4:01 PM, Andy Wingowrote: > Hey all! > > This is an update along the road to Guile 3. Check > https://lists.gnu.org/archive/html/guile-devel/2017-11/msg00016.html for > the previous entry. > > Since 25 November there have been around 100 commits or so. Firstly I > merged in patches from stable-2.0, including patches corresponding to > the improvements in the Guile 2.2.3 stable series release. > > Then, I started to look at "instruction explosion" for vector-ref et > al. Basically the idea is to transform the various subcomponents of > e.g. vector-ref into their constituent parts. In the concrete case of > vector-ref, we have to check that the vector is a heap object, that its > heap object tag is "vector", we have to extract the length from the heap > object, then we have to check that the index is an integer between 0 and > length-1, and finally we dereference the field in the vector. > Instruction explosion turns all of these into different primcalls and > branches. > > One thing that became apparent was that with instruction explosion, we'd > have a lot more control flow. Information that the optimizer would > learn in a specific way (e.g. via specialzied type inference / effects > analysis handlers for vector-ref) would instead be learned by generic > control flow. > > Concretely -- > > scheme@(guile-user)> ,x (lambda (v i) (vector-ref v i)) > Disassembly of #:1:3 (v i)> at > #x29f5b4c: > >0(assert-nargs-ee/locals 3 1);; 4 slots (2 args) at > (unknown file):1:3 >1(immediate-tag=? 2 7 0) ;; heap-object? at > (unknown file):1:17 >3(jne 23);; -> L3 >4(heap-tag=? 2 127 13) ;; vector? >6(jne 20);; -> L3 >7(word-ref/immediate 3 2 0) >8(ursh/immediate 3 3 8) >9(immediate-tag=? 1 3 2) ;; fixnum? > 11(jne 13);; -> L2 > 12(untag-fixnum 0 1) > 13(s64-imm 14(jl 8) ;; -> L1 > 15(s64 16(jnl 6) ;; -> L1 > 17(mov 3 0) > 18(uadd/immediate 3 3 1) > 19(scm-ref 2 2 3) > 20(handle-interrupts) > 21(return-values 2) ;; 1 value > L1: > 22(throw/value+data 1 201);; #(out-of-range "vector-ref" > "Argument 2 out of range: ~S") > L2: > 24(throw/value+data 1 225);; #(wrong-type-arg > "vector-ref" "Wrong type argument in position 2 (expecting small integer): > ~S") > L3: > 26(throw/value+data 2 239);; #(wrong-type-arg > "vector-ref" "Wrong type argument in position 1 (expecting vector): ~S") > > So this is a bit horrible and I need to make the disassembler do a > better job, but anyway. Instructions 1 through 6 check that V is a > vector; instructions 7 and 8 extract the length; 9 and
guile 3 update: instruction explosion for pairs, vectors
Hey all! This is an update along the road to Guile 3. Check https://lists.gnu.org/archive/html/guile-devel/2017-11/msg00016.html for the previous entry. Since 25 November there have been around 100 commits or so. Firstly I merged in patches from stable-2.0, including patches corresponding to the improvements in the Guile 2.2.3 stable series release. Then, I started to look at "instruction explosion" for vector-ref et al. Basically the idea is to transform the various subcomponents of e.g. vector-ref into their constituent parts. In the concrete case of vector-ref, we have to check that the vector is a heap object, that its heap object tag is "vector", we have to extract the length from the heap object, then we have to check that the index is an integer between 0 and length-1, and finally we dereference the field in the vector. Instruction explosion turns all of these into different primcalls and branches. One thing that became apparent was that with instruction explosion, we'd have a lot more control flow. Information that the optimizer would learn in a specific way (e.g. via specialzied type inference / effects analysis handlers for vector-ref) would instead be learned by generic control flow. Concretely -- scheme@(guile-user)> ,x (lambda (v i) (vector-ref v i)) Disassembly of #:1:3 (v i)> at #x29f5b4c: 0(assert-nargs-ee/locals 3 1);; 4 slots (2 args) at (unknown file):1:3 1(immediate-tag=? 2 7 0) ;; heap-object? at (unknown file):1:17 3(jne 23);; -> L3 4(heap-tag=? 2 127 13) ;; vector? 6(jne 20);; -> L3 7(word-ref/immediate 3 2 0) 8(ursh/immediate 3 3 8) 9(immediate-tag=? 1 3 2) ;; fixnum? 11(jne 13);; -> L2 12(untag-fixnum 0 1) 13(s64-imm L1 15(s64 L1 17(mov 3 0) 18(uadd/immediate 3 3 1) 19(scm-ref 2 2 3) 20(handle-interrupts) 21(return-values 2) ;; 1 value L1: 22(throw/value+data 1 201);; #(out-of-range "vector-ref" "Argument 2 out of range: ~S") L2: 24(throw/value+data 1 225);; #(wrong-type-arg "vector-ref" "Wrong type argument in position 2 (expecting small integer): ~S") L3: 26(throw/value+data 2 239);; #(wrong-type-arg "vector-ref" "Wrong type argument in position 1 (expecting vector): ~S") So this is a bit horrible and I need to make the disassembler do a better job, but anyway. Instructions 1 through 6 check that V is a vector; instructions 7 and 8 extract the length; 9 and 11 check that the index is a fixnum, 12 extracts the fixnum value as an untagged 64-bit integer, and 13 through 16 check that the index is in range. L1, L2, and L3 are bailouts. The idea here is that if this vector-ref is followed by some other operation on the vector, we'll at least get to elide the vector? checks, and maybe we can reuse the length extraction too. Et cetera. The optimizer makes decisions like when to elide redundant checks based on flow information. However for historical reasons unfortunately the "throw" terms actually did "continue" from the optimizer's perspective; whereas the information flowing to e.g. L3 shouldn't flow at all to instruction 7, the IR didn't have a way to denote terms that didn't continue at all. To fix this I had to make some changes to the IR. On the plus side, $throw is its own term kind now that doesn't have a continuation. Also, $branch is a term instead of an expression shoehorned into $continue; $prompt is a term too. Maybe one day we'll get a $select term for proper case compilation. Finally, the instruction explosion. I refactored the Tree-IL->CPS compiler to allow individual primcalls to have custom lowering routines, and tightened up $primcall so that it now always continues to $kargs. Then I added custom lowering routines for vector-ref et al, and cons and other things. The CPS IR refactors allowed me to remove some useless passes (elide-values, prune-bailouts). There were some optimizer bugs but generally things were already in a good state; e.g. here's a vector sum routine: scheme@(guile-user)> (define (v-sum v) (let lp ((n 0) (sum 0)) (if (< n (vector-length v)) (lp (+ n 1) (+ sum (vector-ref v n))) sum))) scheme@(guile-user)> ,x v-sum Disassembly of # at #x1fa2750: 0(assert-nargs-ee/locals 2 3);; 5 slots (1 arg)at (unknown file):1:0 1(make-short-immediate 4 2) ;; 0 2(immediate-tag=? 3 7 0) ;; heap-object? at (unknown file):1:51 4(jne 39);; -> L5 5(heap-tag=? 3
Re: guile 3 update: more number unboxing improvements
> (define (out-of-range x) (error "out of range" x)) > (define (not-int x) (error "expected an integer" x)) > (cond >((fixnum? x) > (if (<= -10 x 100) > (* x 2) > (out-of-range x))) >((bignum? x) > (if (<= -10 x 100) > (* x 2) > (out-of-range x))) >(else > (not-int x))) Looks a bit like the result of "splitting tails", in this case, tho selectively. Stefan
guile 3 update: more number unboxing improvements
Hi, In the last update (https://lists.gnu.org/archive/html/guile-devel/2017-11/msg00010.html) I talked a lot about fixnums. This last couple weeks' work is along the same lines. Firstly I added support for specialized instructions that compare (in the < or = sense) s64 and u64 values against 8-bit immediates. Then I started to do some "exploding" of the comparison operators: for example, if a number is a 64-bit integer, maybe first see if it's a fixnum and take a fast path there, otherwise call out to a scm->u64 primitive. I was thinking maybe we would be able to inline even scm->u64 entirely, but this turned out to be a bad idea, and indeed even the speculative inlining of the fixnum case of e.g. < wasn't that great. The problem is that if you replace a simple primcall (define x FOO) with a diamond control structure like (define x (if FIXNUM? FOO-1 FOO-2)) early in the compiler, that makes it harder to do code motion like CSE or LICM on X, as X now has two definitions. It could make sense to reify a diamond control flow more towards the compiler back-end, but not in the beginning. Instruction explosion only makes sense if some of the exploded instructions can be eliminated or are themselves subject to CSE. If it's just exploding instructions that can never be eliminated, probably it's better to call out to the runtime (as in the case of scm->u64, unless it can be reduced to untag-fixnum). By this time I had backed myself into a little corner with number specialization, so I had to back out and fix my mess; took a week or so, but then I was back on track, with a new idea. The problem I was looking to solve was to hoist a fixnum? check above code like (= x (logand x #xff)). This predicate will only succeed if X is a fixnum. I then realized it would be tricky, as it would involve hoisting the assertion made inside logand that X is an exact integer "above" (but to where?), and it could be that the assertion doesn't commute with other effects so it can't be hoisted. So I modified the problem :) Instead I wanted to optimize this kind of code: (define (t x) (unless (exact-integer? x) (error "expected an integer" x)) (unless (= x (logand x #xff) (error "out of range" x)) (* x 2)) or (define (t x) (unless (exact-integer? x) (error "expected an integer" x)) (unless (<= -10 x 100) (error "out of range" x)) (* x 2)) Here exact-integer? is effectively (or (fixnum? x) (bignum? x)), so we have a more precise type of X flowing into the predicate. However it's not magical; the "=" in the first example still sees that X could be a bignum, and likewise for the <= operations. What you want to do here is to "devirtualize" part of this function. This is a compiler pass that effectively inlines the concrete implementations of a virtual operation like =, logand, or <=. But you don't want to devirtualize just one operation; you want to devirtualize a trace of operations. Like this: (define (out-of-range x) (error "out of range" x)) (define (not-int x) (error "expected an integer" x)) (cond ((fixnum? x) (if (<= -10 x 100) (* x 2) (out-of-range x))) ((bignum? x) (if (<= -10 x 100) (* x 2) (out-of-range x))) (else (not-int x))) This causes code growth initially, but probably causes shrinkage later; concretely this code optimizes to: (define (out-of-range x) (error "out of range" x)) (define (not-int x) (error "expected an integer" x)) (cond ((fixnum? x) (let ((x* (untag-fixnum x))) (if (s64<= -10 x*) (if (s64<= x* 100) (tag-fixnum (s64+ x* x*)) (out-of-range x)) (out-of-range x ((bignum? x) (out-of-range x) (else (error "expected an integer" x))) which is indeed what we get in code: Disassembly of # at #xb437a0: 0(assert-nargs-ee/locals 2 0);; 2 slots (1 arg)at (unknown file):10:0 1(immediate-tag=? 0 3 2) ;; fixnum?at (unknown file):11:10 3(jne 11);; -> L1 4(untag-fixnum 1 0)at (unknown file):12:10 5(s64-imm L2 7(imm-s64 L2 9(mov 0 1) at (unknown file):13:2 10(uadd 1 0 1) 11(tag-fixnum 0 1) 12(handle-interrupts) 13(return-values 2) ;; 1 value L1: 14(immediate-tag=? 0 7 0) ;; heap-object? at (unknown file):11:10 16(jne 8) ;; -> L3 17(heap-tag=? 0 4095 279) ;; bignum? 19(jne 5) ;; -> L3 L2: 20(throw/value 0 138) ;; #(misc-error #f "out of range ~S") at (unknown file):12:25 22(handle-interrupts) 23(return-values 1) ;; 0 values L3: 24(throw/value 0 150) ;; #(misc-error #f
Re: guile 3 update: better reasoning about fixnums
Hello Andy, > An update on Guile 3 hackings over the past couple weeks. > ... > In summary, a lot of internal plumbing, for what appears to be preparatory > work > for future stuff. I do not have the knowledge and background to make any valuable technical comment, but I wanted to thank you for all this great work you are doing on Guile's native AOT compiler: this really is fantastic Thank you! David pgpO2ZJpthBrO.pgp Description: OpenPGP digital signature
guile 3 update: better reasoning about fixnums
Hi, An update on Guile 3 hackings over the past couple weeks. Firstly I made a change to the CPS language. Before, "primcalls" -- invocations of primitives known to the compiler, some of which ultimately compile to instructions/bytecode -- took all arguments as CPS values. This was even the case for primcalls that took known constant arguments, for example a primcall that does a vector-ref of a known index. I made a change so that all primcalls now have an additional slot, which is for some constant data associated with the primcall. (If you need two pieces of constant data, just cons up a data structure.) The idea is that constants used by primcalls as immediates don't need to participate in optimizations in any way -- they should not participate in CSE, have the same lifetime as the primcall so not part of DCE either, and don't need slot allocation. Indirecting them through a named $const binding is complication for no benefit. This change should improve compilation time and memory usage, as the number of labels and variables goes down. I didn't measure much though. I built on that to add set of "throw" primcalls to which all throws and errors compile, with corresponding VM support. This reduces the code size for error branches in functions that do a lot of type checking using e.g. srfi-9 accessors; for example in Guile 2.2, an internal procedure in the assembler (encode-X8_S8_S8_S8) compiles to 158 32-bit words, whereas in 3.0 it is currently 126 words, even after a bit of instruction explosion, and indeed it's a precondition for facilitating more instruction explosion. I also changed the compilation of "ash" to compile to "lsh", "rsh", or a branch between the two. This simplifies things later in the compiler. There are new "tag-fixnum" and "untag-fixnum" instructions, that convert between uint64_t and SCM values without branches. Many instances of scm->u64 etc now compile to these instead. Some unboxed operations and comparisons now operate in the signed range, as it's more convenient to check for fixnums using the fixnum? primcall (which compiles to an instance of the immediate-tag=? instruction) than it is to e.g. check ranges of generic integers, and there is now better support throughout the compiler for s64 values. I removed the <=, >=, and > comparison operators; they can all be expressed in terms of <, though looking back on this now I see that I messed up the behavior with NaN and <= or >=. Damn. Will fix. I added some signed right shift instructions, to do sign-extending right shifts over unboxed values; and that's about it. In summary, a lot of internal plumbing, for what appears to be preparatory work for future stuff. My to-do list in the near term has a few more plumbing-type tasks: * Remove u64=?, as we can just use s64=? polymorphism * Add s64-imm=?, imm-u64= with NaN values * Continue with instruction explosion, starting with vector-ref Cheers, Andy
Re: guile 3 update
Andy Wingo writes: > Hi :) > > On Sun 22 Oct 2017 15:22, Christopher Allan Webber> writes: > >> - Could native code compilation also be a step towards WASM, assuming >>they lend us their GC? > > Regarding this question: yes! Specifically with the "GC" proposal > (which in reality is much more: > https://github.com/WebAssembly/gc/blob/master/proposals/gc/Overview.md) > I think that we will approach WASM's semantic level. There are some > open questions of course, like how to deal with the stack and delimited > continuations, how to do bignums with overflow, etc; but yeah we're > getting there. > > Andy This is very exciting!
guile 3 update: lowered conditionals
Hi, An update on the Guile 3 effort. Since last week I attacked the conditional instructions, replacing e.g. "br-if-< A B OFFSET" with a sequence of "
Re: guile 3 update
Hi :) On Sun 22 Oct 2017 15:22, Christopher Allan Webberwrites: > - Could native code compilation also be a step towards WASM, assuming >they lend us their GC? Regarding this question: yes! Specifically with the "GC" proposal (which in reality is much more: https://github.com/WebAssembly/gc/blob/master/proposals/gc/Overview.md) I think that we will approach WASM's semantic level. There are some open questions of course, like how to deal with the stack and delimited continuations, how to do bignums with overflow, etc; but yeah we're getting there. Andy
Re: guile 3 update
Andy Wingo writes: > Thoughts welcome if I have forgotten something. Cheers :) Super exciting, Andy! I only have two thoughts: - What an exciting future we have ahead of us! - Could native code compilation also be a step towards WASM, assuming they lend us their GC? Woo woo, keep up the good work! :)
guile 3 update
Hi! I have been thinking on how to get from where we are to good native compilation. I think the next thing is instruction explosion. As described here: https://wingolog.org/archives/2016/02/04/guile-compiler-tasks Currently in Guile's VM there are instructions like vector-ref. This is a little silly: there are also instructions to branch on the type of an object (br-if-tc7 in this case), to get the vector's length, and to do a branching integer comparison. Really we should replace vector-ref with a combination of these test-and-branches, with real control flow in the function, and then the actual ref should use some more primitive unchecked memory reference instruction. Optimization could end up hoisting everything but the primitive unchecked memory reference, while preserving safety, which would be a win. But probably in most cases optimization wouldn't manage to do this, which would be a lose overall because you have more instruction dispatch. Well, this transformation is something we need for native compilation anyway. I would accept a patch to do this kind of transformation on the master branch, after version 2.2.0 has forked. In theory this would remove most all high level instructions from the VM, making the bytecode closer to a virtual CPU, and likewise making it easier for the compiler to emit native code as it's working at a lower level. I started looking at this and stumbled when figuring out how to explode struct-ref. In Guile 2.2, our "struct" data type is a bit complicated, and when you go to inline it you really realize that it should be more simple. So the first thing I did was make some changes to structs. I landed some changes in stable-2.2, as described by NEWS: * New interfaces ** `struct-ref/unboxed' and `struct-set!/unboxed' These procedures should be used when accessing struct fields with type `u' (unboxed). See "Structure Basics" in the manual, for full details. * New deprecations ** Struct tail arrays deprecated Guile's structures used to have a facility whereby each instance of a vtable can contain a variable-length tail array of values. The length of the tail array was stored in the structure. This facility was originally intended to allow C code to expose raw C structures with word-sized tail arrays to Scheme. However, the tail array facility was confusing and doesn't work very well. It was very rarely used, but it insinuates itself into all invocations of `make-struct'. For this reason the clumsily-named `make-struct/no-tail' procedure can actually be more elegant in actual use, because it doesn't have a random `0' argument stuck in the middle. Tail arrays also inhibit optimization by allowing instances to affect their shapes. In the absence of tail arrays, all instances of a given vtable have the same number and kinds of fields. This uniformity can be exploited by the runtime and the optimizer. The presence of tail arrays make some of these optimizations more difficult. Finally, the tail array facility is ad-hoc and does not compose with the rest of Guile. If a Guile user wants an array with user-specified length, it's best to use a vector. It is more clear in the code, and the standard optimization techniques will do a good job with it. For all of these reasons, tail arrays are deprecated in Guile 2.2 and will be removed from Guile 3.0. Likewise, `make-struct' / `scm_make_struct' is deprecated in favor of `make-struct/no-tail' / `scm_make_struct_no_tail'. Perhaps one day we will be able to reclaim the `make-struct' name! ** Struct "self" slots deprecated It used to be that you could make a structure vtable that had "self" slots. Instances of that vtable would have those slots initialized to the instance itself. This can be useful in C code where you might have a pointer to the data array, and want to get the `SCM' handle for the structure. However this was a little used complication without any use vin Scheme code. To replace it, just use "p" slots and initialize the slot values manually on initialization. ** Struct fields with opaque ("o") protection deprecated Struct fields are declared with a "protection", meaning read-only ('r'), read-write ('w'), or opaque ('o'). There is also "hidden" ('o') which is read-write but which isn't initialized by arguments passed to `make-struct/no-tail', but that's a detail. Opaque struct fields were used to allocate storage in a struct that could only be accessed by C. This facility was very rarely used (unused in Guile itself) but now that we are implementing more and more in Scheme, it is completely useless. To enforce permissions on struct fields, instead layer on an