Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Multiple dispatch #2364

Open
lolbinarycat opened this issue Nov 5, 2020 · 158 comments
Open

Multiple dispatch #2364

lolbinarycat opened this issue Nov 5, 2020 · 158 comments

Comments

@lolbinarycat
Copy link

I've been working with Julia a lot lately and think multiple dispatch would fit Factor quite well. It would solve the problems with math functions not being extendable.

@mrjbq7
Copy link
Member

mrjbq7 commented Nov 5, 2020

I agree!

We have very efficient single dispatch, as well as a not-as-efficient multiple dispatch implementation in the multi-methods vocabulary.

I'd love to merge them by either making the single-dispatch version of multi-dispatch just as fast as it is now, or improving the multi-dispatch to be as fast as single-dispatch is.

@lolbinarycat
Copy link
Author

One thing julia has is abstract types, which I don't think factor has a great alternative to. There's singleton classes, but those are instances of themselves, which isn't the most desirable behavior (the class of numbers is not a number). I also don't think you can inherit from a singleton type, which kinda defeats the purpose.

@lolbinarycat
Copy link
Author

lolbinarycat commented Nov 6, 2020

abstract classes:

USING: kernel classes classes.private words parser math sequences combinators classes.tuple lexer vocabs.parser ;
IN: classes.abstract

PREDICATE: abstract-class < class abstract-class word-prop ;

: abstract-class-instance? ( abs-class class -- ? )
    [  2dup =
        [ 2drop t ]
        [ superclass-of abstract-class-instance? ] if
    ] [ drop f ] if* ; ! this if* uses the fact that f is used as the top class 


M: abstract-class instance? swap class-of superclass-of abstract-class-instance? ;

: define-abstract-class ( word superclass -- )
    [ { } clone dup clone abstract-class define-class ]
    [ "superclass" set-word-prop ]
    [ drop
        [ t abstract-class set-word-prop ]
        [ set-last-word ] bi
    ] 2tri ;

: parse-abstract-class-definition ( -- word superclass )
    scan-token current-vocab create-word \ ; parse-until
    { { [ dup length 0 = ] [ drop f ] } ! no superclass specified 
      { [ dup [ first \ < = ] [ length 2 = ] bi and ] [ second ] } 
      [ "syntax error" throw ] } cond ;

SYNTAX: ABSTRACT: parse-abstract-class-definition define-abstract-class ;

! allows tuples to inherit from abstract classes (kinda important)
M: abstract-class final-class? drop f ;

@timor
Copy link
Contributor

timor commented Nov 9, 2020

Multiple dispatch would be really nice!
There is also some previous discussion about this: #2257

@nomennescio
Copy link
Contributor

True, multiple dispatch would certainly serve a purpose, seeing how successful Julia is using it for code reuse. What about the current "experimental" multiple dispatch code by Slava (vocabulary multi-methods)?

@nomennescio
Copy link
Contributor

Just going through one of Slava's (@slavapestov) old posts on the optimizing compiler, in which he writes:
"I'm going to investigate inline caching to speed up generic word call sites which are monomorphic, but the compiler is unable to infer this statically. I will also look into various techniques for speeding up polymorphic dispatch."
Was there any progress made on speeding up polymorphic dispatch after that article was written?

@nomennescio
Copy link
Contributor

It seems Slava (@slavapestov) did indeed progress beyond this point in his post on polymorphic inline caching. Not sure if more recent progress was made on speeding up polymorphic dispatch. Nor how that would look like for multiple dispatch.

@nomennescio
Copy link
Contributor

Yup, found another, but older post on multi-methods, which gave some more details : "The powerful thing about this new implementation of hooks is that not only can you dispatch on multiple variables, but you can add methods to any old generic which dispatches on a variable and the original designer of the generic does not have to explicitly take this into account." "The other nice thing about this is that the multi-method GENERIC: word unifies and generalizes four words in the core, GENERIC:, GENERIC# for dispatching on a value further down on the stack, HOOK: for dispatching on a variable, and MATH: which performs double dispatch on numbers only." "All that remains to be done is to merge the multi-methods code. The code is still not quite ready to go in the core, though. The only feature that single dispatch has and multiple-dispatch lacks is call-next-method, which is easy to implement. A bigger hurdle to clear is performance; right now multi-methods are implemented in a naive way, where the dispatch time is O(mn) with m methods and n stack positions and variables to dispatch on. This can be improved significantly and I will find time reading the literature on optimizing method dispatch over the next few weeks."

Note, this is from a 2008 post, 4 years before the launch of the Julia language. (B.t.w. Jeff Bezanson's thesis has details on how Julia implements its type system and multi-methods)

@erg
Copy link
Member

erg commented Nov 12, 2020

How about a syntax like this for multimethods?

  • a GENERIC word still lives somewhere (it's not global)
  • M: methods require stack effects again
  • extended stack effect syntax ( dynvar1 dynvar2 | *pos2* pos1 pos0 -- out )
  • methods are the same syntax as normal words but with M:
  • you can have several GENERIC words with the same name as long as you scope it right
  • words can't suddenly become generic without a syntax change
  • with only one argument to dispatch it will still be fast
  • multimethod dispatch on dynamic variables first, then on positional args

Open problems/questions:

  • make it fast with Julia's algorithm?
  • can you think of something better than *var* for positional args?
  • can "accessors" vocabulary go away? (Generate fine-grained generics that live in the vocabulary that the TUPLE: is declared in, or merge them somehow with a GLOBAL-GENERIC: a>> ( *obj* -- a ) or...?)
GENERIC: length ( *obj* -- len )
M: length ( array -- n ) length>> ;
! : length ( array -- n ) length>> ;  would be an error if GENERIC: length is in scope, needs M:

GENERIC: shift ( *x* n -- y ) foldable
M: shift ( bignum n -- y ) integer>fixnum bignum-shift ; inline
M: shift ( fixnum n -- y ) integer>fixnum fixnum-shift ; inline

!  multiple dispatch off a and b
GENERIC: rock-paper-scissors ( *a* *b* -- a-wins? )
M: rock-paper-scissors ( scissors paper -- ? ) 2drop t ;
M: rock-paper-scissors ( rock scissors -- ? ) 2drop t ;
M: rock-paper-scissors ( paper rock -- ? ) 2drop t ;
M: rock-paper-scissors ( thing thing -- ? ) 2drop f ;

! dispatch off os dynamic variable (like HOOK:)
GENERIC: resolve-symlinks ( os | path -- path' )
M: resolve-symlinks ( windows | path -- path' ) normalize-path ;
M: resolve-symlinks ( unix  | path -- path' )
    path-components "/"
    [ append-path dup exists? [ follow-links ] when ] reduce ;

! dispatch off two dynamic variables
GENERIC: get-editor ( os editor-class | -- path )
M: get-editor ( windows visual-studio-code | -- path ) "code.cmd" find-in-applications ;
M: get-editor ( windows visual-studio-code-insiders | -- path ) "code-insiders.cmd" find-in-applications ;
M: get-editor ( unix    visual-studio-code | -- path ) "code" which ;
M: get-editor ( unix    visual-studio-code-insiders | -- path ) "code-insiders" which ;

With @kusumotonorio's suggestion of putting dynvars last:

GENERIC: length ( *obj* -- len )
M: length ( array -- n ) length>> ;
! : length ( array -- n ) length>> ;  would be an error if GENERIC: length is in scope, needs M:

GENERIC: shift ( *x* n -- y ) foldable
M: shift ( bignum n -- y ) integer>fixnum bignum-shift ; inline
M: shift ( fixnum n -- y ) integer>fixnum fixnum-shift ; inline

!  multiple dispatch off a and b
GENERIC: rock-paper-scissors ( *a* *b* -- a-wins? )
M: rock-paper-scissors ( scissors paper -- ? ) 2drop t ;
M: rock-paper-scissors ( rock scissors -- ? ) 2drop t ;
M: rock-paper-scissors ( paper rock -- ? ) 2drop t ;
M: rock-paper-scissors ( thing thing -- ? ) 2drop f ;

! dispatch off os dynamic variable (like HOOK:)
GENERIC: resolve-symlinks ( path | os -- path' )
M: resolve-symlinks ( path | windows -- path' ) normalize-path ;
M: resolve-symlinks ( path | unix -- path' )
    path-components "/"
    [ append-path dup exists? [ follow-links ] when ] reduce ;

! dispatch off two dynamic variables
GENERIC: get-editor ( os editor-class | -- path )
M: get-editor ( | windows visual-studio-code -- path ) "code.cmd" find-in-applications ;
M: get-editor ( | windows visual-studio-code-insiders -- path ) "code-insiders.cmd" find-in-applications ;
M: get-editor ( | unix    visual-studio-code -- path ) "code" which ;
M: get-editor ( | unix    visual-studio-code-insiders -- path ) "code-insiders" which ;

@kusumotonorio
Copy link
Contributor

I think your suggestion of having the sequence of the dynamic variable before the output sequence is better than what I've presented before.
However, personally, even in that case, I prefer the sequence of dynamic variables to meet later than the sequence of inputs in the stack. And it seems to me that it would be easier to implement the parser.

( *pos2* pos1 pos0 | dynvar1 dynvar2 -- out )

This is because dynamic variables that do not exist in the regular word definition are not always present in the method definition.

If a generic word definition contains the names of the input (e.g. *x*) and a method definition contains only type information for dispatch of input (e.g. scissors, paper), what names should I use to refer to them in the case of M::? I suppose it's possible to say that you can use the names in the generic word definitions, but this would reduce documentation for humans because you wouldn't necessarily have the generic definitions near the method descriptions.

@nomennescio
Copy link
Contributor

All posts I could find by Slava on multiple dispatch / multi-methods:

https://factor-language.blogspot.com/2008/04/multi-methods-and-hooks.html
https://factor-language.blogspot.com/2008/04/inheritance-is-done.html
https://factor-language.blogspot.com/2008/06/syntax-proposal-for-multi-methods.html
https://factor-language.blogspot.com/2008/01/multi-methods-in-factor.html

https://factor-language.blogspot.com/2008/08/factor-is-now-five-years-old.html
https://factor-language.blogspot.com/2008/03/working-on-inheritance.html

Interestingly enough, Slava mentioned multiple dispatch to be top priority back in 2008

"very soon I will start moving forward with the language again, with multiple dispatch at the top of the list."
"I really want to get multi-methods done by the end of June, or sometime in July perhaps, and then focus on the compiler, documentation, bug fixes, and libraries. I'm going to draw a line in the sand soon; no more language changes until 1.0."

and he saw multiple dispatch to be a complete replacement for current single disptach, if it could be made fast enough

"Let's put this another way. The following parsing words will be eliminated: M: GENERIC: MEMO:: MACRO:: M:: GENERIC# HOOK: In return, you gain a huge amount of expressive power: multiple dispatch. Wow!"

"I will tackle multi-methods; what this really means is efficient implementation of multi-methods, given that an inefficient implementation already exists in extra/. This presents its own set of unique challenges but I look forward to solving that problem. [..] Once Factor has multi-methods, our object system will be comparable in power to CLOS, however I believe it will offer genuine improvements and innovations in several respects. Furthermore, the implementation will be a lot simpler than PCL."

I could not find any info beyond that, especially not on how the performance improvement should be realized (other than referring to studying literature on the subject).

@nomennescio
Copy link
Contributor

How about a syntax like this for multimethods?

It would be interesting to compare this to Slava's post on a proposed new syntax

@nomennescio
Copy link
Contributor

B.t.w. googling "optimizing multi-method dispatch" will give a whole array of papers and proposals for speeding up multi-methods. The catch is in the different type systems, and on implementation details/design choices of specific languages, compared to Factor's details, and how that will translate to a solution capable of replacing all single dispatches with same or better performance, as well as improving on the current O(mn) implementation in 'extras'.

@spacefrogg
Copy link
Contributor

I want to mention that CommonLisp, especially SBCL, has very fast multi-methods for a long time. And with their Meta Object Protocol, define an interesting concept of having a default implementation of dispatch but leaving it open to the implementor to choose a different one for specific purposes. This becomes important when thinking about removing GENERIC:, as Slava proposed, as you may want to specify what variant of generic functions you want to build. So, you need a syntactic construct to make that specification.

@nomennescio
Copy link
Contributor

nomennescio commented Nov 12, 2020

The "syntax" post by Slave seems to go into the direction of typed stack declarations, where if you omit the type, it defaults to 'object' (i.e. "determine at runtime"), otherwise it means "a type or its subtype". Pretty comparable to a lot of (even statically) typed OO languages, except for the contraints he puts that methods/words with the same name should have the same stack effect w.r.t. the number of items on the stack. Are we moving towards making everything TYPED: (with object as fallback)? How transparant will 'dynamic' versus 'static' types be? Note that the Julia community is still struggling with the nitty gritty details of their typesystem and multi-dispatch (e.g. https://youtu.be/TPuJsgyu87U, JuliaLang/julia#23740).

@spacefrogg
Copy link
Contributor

Regarding dispatch: Methods are always TYPED: and always have been. After all, factor is still a strong-typed language. So, of course, everything has a known type. Thus, ordinary words are, in effect, already always typed with object.

@kusumotonorio
Copy link
Contributor

It seems to me that Slava was intended to achieve something similar to the generic functions realized in other languages. However, I think it would be nice if the stack language Factor had a unique multi-dispatch function that was different from those.
I think a natural extension of the current dispatch mechanism would be to declare that only some parameters are involved in the specialization of a method, i.e., using GENERIC:, which I think is typical of Factor. And since it is not significantly different from the current functionality, I think it has a good chance of becoming a reality. I don't want to spend another decade trying to make multi-dispatch the norm. ;-)

@alex-ilin
Copy link
Member

@erg: I like the idea of adding stack effects to all M: methods. I'm a bit confused about your unix example compared to windows:

M: resolve-symlinks ( windows | path -- path' ) normalize-path ;
M: unix resolve-symlinks ( unix  | path -- path' )

Are you proposing two alternative syntax options there? Do you propose to support both?

@spacefrogg: "Methods are always TYPED: and always have been."
Really? When I use TYPED:, I expect to be able to declare types for all parameters, not only to the one used in the dispatch. Is it really the case that I could specify types for all parameters in M:?

@alex-ilin
Copy link
Member

Pop quiz: which is more abstract - M: object (...) or M: f (...)?

@erg
Copy link
Member

erg commented Nov 12, 2020

@alexiljin the unix was a typo. oops!

@jonenst
Copy link
Contributor

jonenst commented Nov 12, 2020

What about somethings that looks more like the existing ( quot: ( -- ) -- ) system ? Maybe the same ":" to indicate metainformation on the input ? Or maybe the '#" like the GENERIC#: word ?

GENERIC: shift ( x# n -- y )
or
GENERIC: shift ( x: * n -- y )
or
GENERIC: shift ( x: D n -- y ) -- as in Dispatching
GENERIC: resolve-symlinks ( os: H path -- path' ) -- as in Hook

I'm not sure I like the dynvars in the stack effect, is it more comprehensible to keep things separate?

Is it really worth it to have this information stuffed in the stack effect as symbols, vs using something like

GENERIC#: word arr ( stack -- effect )
GENERIC#: { 1 0 } rock-paper-scissors ( a b -- a-wins? ) -- dispatch on 1 deep first, then 0 deep.
-- This is not very nice, but it's simple..

I don't know, "quot: " is nice in the stack effect, so maybe the dispatch information can go there too.

@kusumotonorio
Copy link
Contributor

kusumotonorio commented Nov 12, 2020

My suggestion of extending : instead of M: to define methods for multi-dispatch is intended to simplify it in the long run by doing away with M:, but in the short run, maintaining source code compatibility for the time being by keeping M: intact is When we create a new way of defining methods, it will be difficult to rewrite all the source code in that fashion at once.

@erg
Copy link
Member

erg commented Nov 12, 2020

I'm worried about fast code being deoptimized because generic or multimethod dispatch is silently introduced, especially if it's in another file somehow. That's why the GENERIC: and M: syntax is better than just overloading :.

@alex-ilin
Copy link
Member

alex-ilin commented Nov 12, 2020

No takers on the pop quiz?

@odiferousmint
Copy link

Someone might reach out to Slava for some ideas or pointers... surely he is allowed to discuss it. :/

@spacefrogg
Copy link
Contributor

I'm worried about fast code being deoptimized because generic or multimethod dispatch is silently introduced, especially if it's in another file somehow. That's why the GENERIC: and M: syntax is better than just overloading :.

I don't see an immediate issue, here. Careful implementation should be able to yield a fast "normal" word lookup in case there is only one method specializing on object.

@spacefrogg
Copy link
Contributor

Really? When I use TYPED:, I expect to be able to declare types for all parameters, not only to the one used in the dispatch. Is it really the case that I could specify types for all parameters in M:?

That was not what I was referring to, but I was imprecise. What I meant was, everything is implicitly typed in factor. That means when you don't make type assumptions, you implicitly assume object. What I meant especially was M: foo ( a b c -- ) is implicitly specializing on object object foo --. So, : foo' ( a b c -- ) has the same effect as M: foo' object ( a b c -- ). This is why I think it is safe (and ingenious) to extend :, as Slava was proposing, regardless of multi-methods. Of course, the details of the implementation matter much. The implementation would have to ensure that it can detect the case of a single method and remove the indirection at the call site. Then, using TYPED:, would mean to just specify a method (and constrain the types of the other arguments in case of single-dispatch).

@kusumotonorio
Copy link
Contributor

kusumotonorio commented Feb 12, 2021

By the way, actually, "benchmark" run fails at the benchmark of boot in the middle. Also, make-my-image gives me an error.
I have built multi-generic step by step, and it works fine for me, but I don't think I can get other people to try it. That makes no sense. I would like to improve this situation somehow, but it seems to be very difficult...

The current multi-generic requires a lot of file changes, and those files are USING:ing each other." The chain of "USING:" fails.
For example, if I do make-my-image now, I will get the following.

resource:core/math/integers/integers.factor

resource:core/syntax/syntax.factor

3: USING: accessors arrays byte-arrays byte-vectors classes
4: classes.algebra.private classes.builtin classes.error
                                                        ^
resource:core/classes/error/error.factor

3: USING: accessors classes.private classes.tuple
4: classes.tuple.private combinators kernel parser sequences words ;
                                                  ^
resource:core/parser/parser.factor

3: USING: accessors arrays classes combinators compiler.units
                                                             ^
resource:core/compiler/units/units.factor

7: QUALIFIED-WITH: multi-generic mg
                                   ^
Vocabulary does not exist
name "multi-generic"

@mrjbq7
Copy link
Member

mrjbq7 commented Feb 12, 2021 via email

@kusumotonorio
Copy link
Contributor

This is probably the only thing I've changed/made:

master...kusumotonorio:multi-generic

@timor
Copy link
Contributor

timor commented Feb 14, 2021

I have been working on integrating multiple dispatch with the existing inline cache mechanism, and took another look at your benchmarking code. I think it basically took caching completely out of the picture even with tuple classes, because in the bm words, all calls to the methods are optimized to dispatch on the class at compile time. The partial inlining support extends that to your multi-generic implementation. As mentioned previously, the existing implementation seems to put more emphasis on run-time dispatch efficiency instead of compile-time open-coding. It might make sense to include testing these cases too to get the whole picture, e.g. like this:

: wrapped-md-beats? ( obj1 obj2 -- ? )
    md-beats? ;
: wrapped-sd-beats? ( obj1 obj2 -- ? )
    sd-beats? ;

and in bm:

    gc
    [
        TIMES [
            paper paper       wrapped-sd-beats? drop
            paper scissors    wrapped-sd-beats? drop
            paper rock        wrapped-sd-beats? drop

            scissors paper    wrapped-sd-beats? drop
            scissors scissors wrapped-sd-beats? drop
            scissors rock     wrapped-sd-beats? drop

            rock paper        wrapped-sd-beats? drop
            rock scissors     wrapped-sd-beats? drop
            rock rock         wrapped-sd-beats? drop
        ] times
    ] benchmark
    [ 1.0e9 / ] [ no-dispatch-time get / ] bi
    "non-inlined sd:            %.6f seconds (%.2f times slower)\n" printf

! ...

    gc
    [
        TIMES [
            paper paper       wrapped-md-beats? drop
            paper scissors    wrapped-md-beats? drop
            paper rock        wrapped-md-beats? drop

            scissors paper    wrapped-md-beats? drop
            scissors scissors wrapped-md-beats? drop
            scissors rock     wrapped-md-beats? drop

            rock paper        wrapped-md-beats? drop
            rock scissors     wrapped-md-beats? drop
            rock rock         wrapped-md-beats? drop
        ] times
    ] benchmark
    [ 1.0e9 / ] [ no-dispatch-time get / ] bi
    "non-inlined md:             %.6f seconds (%.2f times slower)\n" printf

Note that Factor has support for benchmarking dispatch with time and dispatch-stats. which are useful when profiling dispatch.

@kusumotonorio
Copy link
Contributor

I added these to the list.

! wrapped

: wrapped-md-beats? ( obj1 obj2 -- ? )
    md-beats? ;

: wrapped-sd-beats? ( obj1 obj2 -- ? )
    sd-beats? ;

: wrapped-smd-beats? ( obj1 obj2 -- ? )
    smd-beats? ;
100,000 repetitions of all combinations of rock-paper-scissors
no-dispatch:                0.003692 seconds (reference)
single-dispatch:            0.002158 seconds (0.58 times slower)
multi-dispatch:             0.001992 seconds (0.54 times slower)
multi-dispatch (typed):     0.001851 seconds (0.50 times slower)
single spec multi-dispatch: 0.002162 seconds (0.59 times slower)
single-hook-dispatch:       0.055957 seconds (15.16 times slower)
multi-hook-dispatch:        0.039109 seconds (10.59 times slower)
single-hook-multi-dispatch: 0.054541 seconds (14.77 times slower)
non-inlined sd:             0.005051 seconds (1.37 times slower)
non-inlined md:             0.007539 seconds (2.04 times slower)
non-inlined smd:            0.004805 seconds (1.30 times slower)

Wow! How effective is the inline cache you made?

@timor
Copy link
Contributor

timor commented Feb 15, 2021

Wow! How effective is the inline cache you made?

I haven't done enough testing yet to give an informed answer to that. I tested it with this some tests from this benchmark (adapted the syntax), and it has not improved anything. That was to be expected though, since the inline cache does almost nothing to accelerate runtime dispatch of predicate classes.

@timor
Copy link
Contributor

timor commented Feb 15, 2021

I added a test for tuples and ran bm. These are the results:

1,000,000 repetitions of all combinations of rock-paper-scissors
no-dispatch:                0.028201 seconds (reference)
single-dispatch:            0.018094 seconds (0.64 times slower)
non-inlined sd:            0.041819 seconds (1.48 times slower)
tuple sd:             0.021763 seconds (0.77 times slower)
non-inline tuple sd:             0.037772 seconds (1.34 times slower)
multi-dispatch:             0.016586 seconds (0.59 times slower)
non-inlined md:             0.063198 seconds (2.24 times slower)
tuple md:             0.016605 seconds (0.59 times slower)
non-inline tuple md:             0.044045 seconds (1.56 times slower)
multi-dispatch (typed):     0.016624 seconds (0.59 times slower)
single spec multi-dispatch: 0.017904 seconds (0.63 times slower)
single-hook-dispatch:       0.429926 seconds (15.24 times slower)

1,000,000 repetitions of the showdown of the all combinations of No.001 to No.005
single-dispatch:            0.047821 seconds (reference)
multi-dispatch:             0.044477 seconds (0.93 times slower)
single spec multi-dispatch: 0.044442 seconds (0.93 times slower)

@kusumotonorio
Copy link
Contributor

I changed my own bm to make it easier to compare.

IN: scratchpad bm

100,000 repetitions of all combinations of rock-paper-scissors
no-dispatch:                0.003762 seconds (reference)
single-dispatch:            0.002192 seconds (0.58 times slower)
multi-dispatch:             0.001881 seconds (0.50 times slower)
multi-dispatch (typed):     0.001888 seconds (0.50 times slower)
single spec multi-dispatch: 0.002346 seconds (0.62 times slower)
single-hook-dispatch:       0.057554 seconds (15.30 times slower)
multi-hook-dispatch:        0.042013 seconds (11.17 times slower)
single-hook-multi-dispatch: 0.058175 seconds (15.46 times slower)
non-inlined sd (wrapped):   0.004956 seconds (1.32 times slower)
non-inlined md (wrapped):   0.007661 seconds (2.04 times slower)
non-inlined smd (wrapped):  0.005266 seconds (1.40 times slower)

repetitions of all combinations of rock-paper-scissors (tuple version)
no-dispatch:                0.006968  seconds (1.85 times slower)
single-dispatch:            0.003125 seconds (0.83 times slower)
multi-dispatch:             0.001907 seconds (0.51 times slower)
multi-dispatch (typed):     0.001889 seconds (0.50 times slower)
single spec multi-dispatch: 0.002942 seconds (0.78 times slower)
single-hook-dispatch:       0.059782 seconds (15.89 times slower)
multi-hook-dispatch:        0.044584 seconds (11.85 times slower)
single-hook-multi-dispatch: 0.057251 seconds (15.22 times slower)
non-inlined sd (wrapped):   0.005517 seconds (1.47 times slower)
non-inlined md (wrapped):   0.009171 seconds (2.44 times slower)
non-inlined smd (wrapped):  0.005525 seconds (1.47 times slower)
IN: scratchpad 

Caching during multi-dispatch is good. It is not a big difference in this example, but the difference is likely to widen when there are more methods or more parameters to specify.

@kusumotonorio
Copy link
Contributor

I have changed multi-generic to be bootable. I feel guilty that I made changes to generic for that.

https://github.com/kusumotonorio/factor/tree/multi-generic
master...kusumotonorio:multi-generic

I hope this will allow other people to try multi-generic.

@timor
Copy link
Contributor

timor commented Feb 17, 2021

@kusumotonorio Why not create a pull request? That way, it is easy for others to keep up-to-date with your branch.

@kusumotonorio
Copy link
Contributor

kusumotonorio commented Feb 17, 2021

@timor That's because I don't think it's the right time to merge it as it is.
If PR makes the discussion easier, then I'm willing to do it.

@timor
Copy link
Contributor

timor commented Feb 17, 2021

You can always mark them as draft to mark them as work in progress. I'd like to try attaching my backend implementation code to your modified syntax frontend.

@kusumotonorio
Copy link
Contributor

@timor I hurriedly added a brief explanation to this PR.

#2431

@kusumotonorio
Copy link
Contributor

kusumotonorio commented Feb 19, 2021

Apparently, I succeeded in incorporating @timor 's algorithm into my multi-generic.
Benchmarking with tuple classes shows speed improvements.

IN: scratchpad bm

100,000 repetitions of all combinations of rock-paper-scissors
no-dispatch:                0.003657 seconds (reference)
single-dispatch:            0.002162 seconds (0.59 times slower)
multi-dispatch:             0.001855 seconds (0.51 times slower)
multi-dispatch (typed):     0.001850 seconds (0.51 times slower)
single spec multi-dispatch: 0.002316 seconds (0.63 times slower)
single-hook-dispatch:       0.060969 seconds (16.67 times slower)
multi-hook-dispatch:        0.042179 seconds (11.53 times slower)
single-hook-multi-dispatch: 0.058840 seconds (16.09 times slower)
non-inlined sd (wrapped):   0.005025 seconds (1.37 times slower)
non-inlined md (wrapped):   0.007578 seconds (2.07 times slower)
non-inlined smd (wrapped):  0.004805 seconds (1.31 times slower)

repetitions of all combinations of rock-paper-scissors (tuple version)
no-dispatch:                0.005456 seconds (1.49 times slower)
single-dispatch:            0.003059 seconds (0.84 times slower)
multi-dispatch:             0.001896 seconds (0.52 times slower)
multi-dispatch (typed):     0.001858 seconds (0.51 times slower)
single spec multi-dispatch: 0.002925 seconds (0.80 times slower)
single-hook-dispatch:       0.061189 seconds (16.73 times slower)
multi-hook-dispatch:        0.046627 seconds (12.75 times slower)
single-hook-multi-dispatch: 0.057612 seconds (15.75 times slower)
non-inlined sd (wrapped):   0.004866 seconds (1.33 times slower)
non-inlined md (wrapped):   0.005300 seconds (1.45 times slower)   <-----
non-inlined smd (wrapped):  0.004910 seconds (1.34 times slower)

Before incorporating @timor 's code, it looked like this:

repetitions of all combinations of rock-paper-scissors (tuple version)
no-dispatch:                0.006968  seconds (1.85 times slower)
single-dispatch:            0.003125 seconds (0.83 times slower)
multi-dispatch:             0.001907 seconds (0.51 times slower)
multi-dispatch (typed):     0.001889 seconds (0.50 times slower)
single spec multi-dispatch: 0.002942 seconds (0.78 times slower)
single-hook-dispatch:       0.059782 seconds (15.89 times slower)
multi-hook-dispatch:        0.044584 seconds (11.85 times slower)
single-hook-multi-dispatch: 0.057251 seconds (15.22 times slower)
non-inlined sd (wrapped):   0.005517 seconds (1.47 times slower)
non-inlined md (wrapped):   0.009171 seconds (2.44 times slower)   <-----
non-inlined smd (wrapped):  0.005525 seconds (1.47 times slower)

@timor
Copy link
Contributor

timor commented Feb 19, 2021

Nice! That was pretty much exactly the case that I wrote that code for. I suspect that will be one of the most common ones in practice.

@kusumotonorio
Copy link
Contributor

kusumotonorio commented Feb 20, 2021

The code I wrote in multi-generic is like a "bowl", which contains the generic as it is, with a few changes. This means that the backend of it is existing code. Any code you add to the generic or modify it can be adapted to the multi-generic almost exactly. I've tested it.
So, I would like to propose to unify multi-generic and generic.multi.

@timor
Copy link
Contributor

timor commented Feb 20, 2021

What started as an attempt of "just" using as much of generic.single as possible to cache multiple dispatch developed into some ideas about how to support more powerful dispatch mechanisms. I added into #2430 some ideas about how a lot of the features from the beginning of this issue can (hopefully) be implemented in a modular way. I added something like Common Lisp's eql specializers as an example, something I personally miss quite a bit.

In any case, feel free to pick whatever you find useful. I think supporting and extending multi-generic efficiently is a good idea to move forward on this.

@timor
Copy link
Contributor

timor commented Feb 20, 2021

@kusumotonorio

Apparently, I succeeded in incorporating @timor 's algorithm into my multi-generic.

Do you have a branch of that somewhere on github?

@kusumotonorio
Copy link
Contributor

kusumotonorio commented Feb 20, 2021

It's just in my hand and I haven't uploaded it to github yet.
I'm adding a switch to tell it to use covariant-tupple dispatch to make it easier to compare the effects of caching, and I'm fixing my own weird code. I'll probably be able to upload it tomorrow.

In order to take advantage of the nice effects of your code, I have to make sure that there are no conflicts with the dispatch algorithm derived from multi-methods. In multi-methods it determines the applicable methods in order from the top of the stack, how does it do that in covariant-tupple dispatch? I would be happy if it is the same.

@timor
Copy link
Contributor

timor commented Feb 20, 2021

Yes it also starts with the top of the stack. However, I implemented a check that ensures that if you have something like
MM: foo ( x: paper y: thing -- ...) and MM: foo ( x: thing y: paper -- ...) you will get an error that it is ambiguous for paper paper foo, until you define one on that. However, when called it should still dispatch on thing paper foo. So if you simply take out that check, it should behave like "asymmetric" dispatch.

@kusumotonorio
Copy link
Contributor

@timor Thanks for the useful info! I hadn't ported the code for that check. That was just a coincidence and not something I did to work around the problem.

Also, what do you think I should consider in order to use multi-generic in combination with the existing multi-dispatch? I guess the class combination algebra.

If I have your permission, I'd like to incorporate it into the main branch of multi-generic with your copyright added, how about that?

@timor
Copy link
Contributor

timor commented Feb 21, 2021

Feel free to use the code any way you want. Don't bother with the copyright.

@kusumotonorio
Copy link
Contributor

kusumotonorio commented Feb 21, 2021

Thank you for your generous consideration.
I have pushed multi-generic that incorporates your code. Please have a look.
I want to make this (or an extension of it) the default feature in multi-generic.

I transcribe the description of the commit:

With this commit, I've ported the initial code from @timor's PR #2430 "Multiple dispatch with existing inline cache infrastructure and abstractions for dispatch specifciers" and added I made some changes to use the cache in multiple dispatch without hook variables. His PR continues to grow and is already different from the latest version.

Currently, it is not the default because it is for testing purposes, and it works when the multi-method generic word without hook variables is not specified as inline or partial-inline, but is specified as cached-multi. Depending on whether cached-multi is added or not, it is possible to compare with conventional multi-dispatch.

e.g.

MGENERIC: cached-md-beats? ( obj1 obj2 -- ? ) cached-multi

MM: cached-md-beats? ( :paper :scissors -- ? ) 2drop t ;
MM: cached-md-beats? ( :scissors :rock -- ? )  2drop t ;
MM: cached-md-beats? ( :rock :paper -- ? )  2drop t ;
MM: cached-md-beats? ( :thing :thing -- ? )  2drop f ;

@kusumotonorio
Copy link
Contributor

kusumotonorio commented Feb 22, 2021

I modified my benchmark for method dispatch which slow down when a multi-generic has many methods, to see how the speed changes with the cache-enabled dispatch algorithm using covarinat-tuple. (It is bm2 in benchmark.factor.)

Based on the results of the previous rock-paper-scissors benchmark and @timor's previous comments, I expected that the new algorithm would be very effective for tuple dispatching, but not for singleton dispatching. However, the results were not what I expected.

100,000 repetitions of the showdown of the all combinations of No.001 to No.005 (singleton version)
(All methods/words are wrapped in words.)
single-dispatch:            0.011787 seconds (reference)
multi-dispatch:             0.080005 seconds (6.79 times slower)
cached-multi-dispatch:      0.012015 seconds (1.02 times slower)  <----
single spec multi-dispatch: 0.011681 seconds (0.99 times slower)

100,000 repetitions of the showdown of the all combinations of No.001 to No.005 (tuple version)
(All methods/words are wrapped in words.)
single-dispatch:            0.010432 seconds (0.88 times slower)
multi-dispatch:             0.062690 seconds (5.32 times slower)
cached-multi-dispatch:      0.009669 seconds (0.82 times slower)  <----
single spec multi-dispatch: 0.010869 seconds (0.92 times slower)

Its new algorithmic dispatching has a great effect on dispatching by singleton as well as tuple dispatching. That's nice to see, but it begs the question, why didn't the Rock-Paper-Scissors benchmark show such results?

@timor, isn't this a strange result according to your perception?

@timor
Copy link
Contributor

timor commented Feb 22, 2021

@kusumotonorio
I think you picked a version of my code which still contains an error. Could you tell me which commit you copied the code out of?

@kusumotonorio
Copy link
Contributor

Perhaps it's time for "generic.multi: Some convience syntax for dispatch tuples d72f938".

@timor timor mentioned this issue Feb 23, 2021
@mrjbq7 mrjbq7 added this to the 0.100 milestone Jul 30, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests