Skip to content
  • Jeremy Evans's avatar
    50c54d40
    Evaluate multiple assignment left hand side before right hand side · 50c54d40
    Jeremy Evans authored
    In regular assignment, Ruby evaluates the left hand side before
    the right hand side.  For example:
    
    ```ruby
    foo[0] = bar
    ```
    
    Calls `foo`, then `bar`, then `[]=` on the result of `foo`.
    
    Previously, multiple assignment didn't work this way.  If you did:
    
    ```ruby
    abc.def, foo[0] = bar, baz
    ```
    
    Ruby would previously call `bar`, then `baz`, then `abc`, then
    `def=` on the result of `abc`, then `foo`, then `[]=` on the
    result of `foo`.
    
    This change makes multiple assignment similar to single assignment,
    changing the evaluation order of the above multiple assignment code
    to calling `abc`, then `foo`, then `bar`, then `baz`, then `def=` on
    the result of `abc`, then `[]=` on the result of `foo`.
    
    Implementing this is challenging with the stack-based virtual machine.
    We need to keep track of all of the left hand side attribute setter
    receivers and setter arguments, and then keep track of the stack level
    while handling the assignment processing, so we can issue the
    appropriate topn instructions to get the receiver.  Here's an example
    of how the multiple assignment is executed, showing the stack and
    instructions:
    
    ```
    self                                      # putself
    abc                                       # send
    abc, self                                 # putself
    abc, foo                                  # send
    abc, foo, 0                               # putobject 0
    abc, foo, 0, [bar, baz]                   # evaluate RHS
    abc, foo, 0, [bar, baz], baz, bar         # expandarray
    abc, foo, 0, [bar, baz], baz, bar, abc    # topn 5
    abc, foo, 0, [bar, baz], baz, abc, bar    # swap
    abc, foo, 0, [bar, baz], baz, def=        # send
    abc, foo, 0, [bar, baz], baz              # pop
    abc, foo, 0, [bar, baz], baz, foo         # topn 3
    abc, foo, 0, [bar, baz], baz, foo, 0      # topn 3
    abc, foo, 0, [bar, baz], baz, foo, 0, baz # topn 2
    abc, foo, 0, [bar, baz], baz, []=         # send
    abc, foo, 0, [bar, baz], baz              # pop
    abc, foo, 0, [bar, baz]                   # pop
    [bar, baz], foo, 0, [bar, baz]            # setn 3
    [bar, baz], foo, 0                        # pop
    [bar, baz], foo                           # pop
    [bar, baz]                                # pop
    ```
    
    As multiple assignment must deal with splats, post args, and any level
    of nesting, it gets quite a bit more complex than this in non-trivial
    cases. To handle this, struct masgn_state is added to keep
    track of the overall state of the mass assignment, which stores a linked
    list of struct masgn_attrasgn, one for each assigned attribute.
    
    This adds a new optimization that replaces a topn 1/pop instruction
    combination with a single swap instruction for multiple assignment
    to non-aref attributes.
    
    This new approach isn't compatible with one of the optimizations
    previously used, in the case where the multiple assignment return value
    was not needed, there was no lhs splat, and one of the left hand side
    used an attribute setter.  This removes that optimization. Removing
    the optimization allowed for removing the POP_ELEMENT and adjust_stack
    functions.
    
    This adds a benchmark to measure how much slower multiple
    assignment is with the correct evaluation order.
    
    This benchmark shows:
    
    * 4-9% decrease for attribute sets
    * 14-23% decrease for array member sets
    * Basically same speed for local variable sets
    
    Importantly, it shows no significant difference between the popped
    (where return value of the multiple assignment is not needed) and
    !popped (where return value of the multiple assignment is needed)
    cases for attribute and array member sets.  This indicates the
    previous optimization, which was dropped in the evaluation
    order fix and only affected the popped case, is not important to
    performance.
    
    Fixes [Bug #4443]
    50c54d40
    Evaluate multiple assignment left hand side before right hand side
    Jeremy Evans authored
    In regular assignment, Ruby evaluates the left hand side before
    the right hand side.  For example:
    
    ```ruby
    foo[0] = bar
    ```
    
    Calls `foo`, then `bar`, then `[]=` on the result of `foo`.
    
    Previously, multiple assignment didn't work this way.  If you did:
    
    ```ruby
    abc.def, foo[0] = bar, baz
    ```
    
    Ruby would previously call `bar`, then `baz`, then `abc`, then
    `def=` on the result of `abc`, then `foo`, then `[]=` on the
    result of `foo`.
    
    This change makes multiple assignment similar to single assignment,
    changing the evaluation order of the above multiple assignment code
    to calling `abc`, then `foo`, then `bar`, then `baz`, then `def=` on
    the result of `abc`, then `[]=` on the result of `foo`.
    
    Implementing this is challenging with the stack-based virtual machine.
    We need to keep track of all of the left hand side attribute setter
    receivers and setter arguments, and then keep track of the stack level
    while handling the assignment processing, so we can issue the
    appropriate topn instructions to get the receiver.  Here's an example
    of how the multiple assignment is executed, showing the stack and
    instructions:
    
    ```
    self                                      # putself
    abc                                       # send
    abc, self                                 # putself
    abc, foo                                  # send
    abc, foo, 0                               # putobject 0
    abc, foo, 0, [bar, baz]                   # evaluate RHS
    abc, foo, 0, [bar, baz], baz, bar         # expandarray
    abc, foo, 0, [bar, baz], baz, bar, abc    # topn 5
    abc, foo, 0, [bar, baz], baz, abc, bar    # swap
    abc, foo, 0, [bar, baz], baz, def=        # send
    abc, foo, 0, [bar, baz], baz              # pop
    abc, foo, 0, [bar, baz], baz, foo         # topn 3
    abc, foo, 0, [bar, baz], baz, foo, 0      # topn 3
    abc, foo, 0, [bar, baz], baz, foo, 0, baz # topn 2
    abc, foo, 0, [bar, baz], baz, []=         # send
    abc, foo, 0, [bar, baz], baz              # pop
    abc, foo, 0, [bar, baz]                   # pop
    [bar, baz], foo, 0, [bar, baz]            # setn 3
    [bar, baz], foo, 0                        # pop
    [bar, baz], foo                           # pop
    [bar, baz]                                # pop
    ```
    
    As multiple assignment must deal with splats, post args, and any level
    of nesting, it gets quite a bit more complex than this in non-trivial
    cases. To handle this, struct masgn_state is added to keep
    track of the overall state of the mass assignment, which stores a linked
    list of struct masgn_attrasgn, one for each assigned attribute.
    
    This adds a new optimization that replaces a topn 1/pop instruction
    combination with a single swap instruction for multiple assignment
    to non-aref attributes.
    
    This new approach isn't compatible with one of the optimizations
    previously used, in the case where the multiple assignment return value
    was not needed, there was no lhs splat, and one of the left hand side
    used an attribute setter.  This removes that optimization. Removing
    the optimization allowed for removing the POP_ELEMENT and adjust_stack
    functions.
    
    This adds a benchmark to measure how much slower multiple
    assignment is with the correct evaluation order.
    
    This benchmark shows:
    
    * 4-9% decrease for attribute sets
    * 14-23% decrease for array member sets
    * Basically same speed for local variable sets
    
    Importantly, it shows no significant difference between the popped
    (where return value of the multiple assignment is not needed) and
    !popped (where return value of the multiple assignment is needed)
    cases for attribute and array member sets.  This indicates the
    previous optimization, which was dropped in the evaluation
    order fix and only affected the popped case, is not important to
    performance.
    
    Fixes [Bug #4443]
Loading