Skip to content
  • Jeremy Evans's avatar
    d2c41b1b
    Reduce allocations for keyword argument hashes · d2c41b1b
    Jeremy Evans authored
    Previously, passing a keyword splat to a method always allocated
    a hash on the caller side, and accepting arbitrary keywords in
    a method allocated a separate hash on the callee side.  Passing
    explicit keywords to a method that accepted a keyword splat
    did not allocate a hash on the caller side, but resulted in two
    hashes allocated on the callee side.
    
    This commit makes passing a single keyword splat to a method not
    allocate a hash on the caller side.  Passing multiple keyword
    splats or a mix of explicit keywords and a keyword splat still
    generates a hash on the caller side.  On the callee side,
    if arbitrary keywords are not accepted, it does not allocate a
    hash.  If arbitrary keywords are accepted, it will allocate a
    hash, but this commit uses a callinfo flag to indicate whether
    the caller already allocated a hash, and if so, the callee can
    use the passed hash without duplicating it.  So this commit
    should make it so that a maximum of a single hash is allocated
    during method calls.
    
    To set the callinfo flag appropriately, method call argument
    compilation checks if only a single keyword splat is given.
    If only one keyword splat is given, the VM_CALL_KW_SPLAT_MUT
    callinfo flag is not set, since in that case the keyword
    splat is passed directly and not mutable.  If more than one
    splat is used, a new hash needs to be generated on the caller
    side, and in that case the callinfo flag is set, indicating
    the keyword splat is mutable by the callee.
    
    In compile_hash, used for both hash and keyword argument
    compilation, if compiling keyword arguments and only a
    single keyword splat is used, pass the argument directly.
    
    On the caller side, in vm_args.c, the callinfo flag needs to
    be recognized and handled.  Because the keyword splat
    argument may not be a hash, it needs to be converted to a
    hash first if not.  Then, unless the callinfo flag is set,
    the hash needs to be duplicated.  The temporary copy of the
    callinfo flag, kw_flag, is updated if a hash was duplicated,
    to prevent the need to duplicate it again.  If we are
    converting to a hash or duplicating a hash, we need to update
    the argument array, which can including duplicating the
    positional splat array if one was passed.  CALLER_SETUP_ARG
    and a couple other places needs to be modified to handle
    similar issues for other types of calls.
    
    This includes fairly comprehensive tests for different ways
    keywords are handled internally, checking that you get equal
    results but that keyword splats on the caller side result in
    distinct objects for keyword rest parameters.
    
    Included are benchmarks for keyword argument calls.
    Brief results when compiled without optimization:
    
      def kw(a: 1) a end
      def kws(**kw) kw end
      h = {a: 1}
    
      kw(a: 1)       # about same
      kw(**h)        # 2.37x faster
      kws(a: 1)      # 1.30x faster
      kws(**h)       # 2.19x faster
      kw(a: 1, **h)  # 1.03x slower
      kw(**h, **h)   # about same
      kws(a: 1, **h) # 1.16x faster
      kws(**h, **h)  # 1.14x faster
    d2c41b1b
    Reduce allocations for keyword argument hashes
    Jeremy Evans authored
    Previously, passing a keyword splat to a method always allocated
    a hash on the caller side, and accepting arbitrary keywords in
    a method allocated a separate hash on the callee side.  Passing
    explicit keywords to a method that accepted a keyword splat
    did not allocate a hash on the caller side, but resulted in two
    hashes allocated on the callee side.
    
    This commit makes passing a single keyword splat to a method not
    allocate a hash on the caller side.  Passing multiple keyword
    splats or a mix of explicit keywords and a keyword splat still
    generates a hash on the caller side.  On the callee side,
    if arbitrary keywords are not accepted, it does not allocate a
    hash.  If arbitrary keywords are accepted, it will allocate a
    hash, but this commit uses a callinfo flag to indicate whether
    the caller already allocated a hash, and if so, the callee can
    use the passed hash without duplicating it.  So this commit
    should make it so that a maximum of a single hash is allocated
    during method calls.
    
    To set the callinfo flag appropriately, method call argument
    compilation checks if only a single keyword splat is given.
    If only one keyword splat is given, the VM_CALL_KW_SPLAT_MUT
    callinfo flag is not set, since in that case the keyword
    splat is passed directly and not mutable.  If more than one
    splat is used, a new hash needs to be generated on the caller
    side, and in that case the callinfo flag is set, indicating
    the keyword splat is mutable by the callee.
    
    In compile_hash, used for both hash and keyword argument
    compilation, if compiling keyword arguments and only a
    single keyword splat is used, pass the argument directly.
    
    On the caller side, in vm_args.c, the callinfo flag needs to
    be recognized and handled.  Because the keyword splat
    argument may not be a hash, it needs to be converted to a
    hash first if not.  Then, unless the callinfo flag is set,
    the hash needs to be duplicated.  The temporary copy of the
    callinfo flag, kw_flag, is updated if a hash was duplicated,
    to prevent the need to duplicate it again.  If we are
    converting to a hash or duplicating a hash, we need to update
    the argument array, which can including duplicating the
    positional splat array if one was passed.  CALLER_SETUP_ARG
    and a couple other places needs to be modified to handle
    similar issues for other types of calls.
    
    This includes fairly comprehensive tests for different ways
    keywords are handled internally, checking that you get equal
    results but that keyword splats on the caller side result in
    distinct objects for keyword rest parameters.
    
    Included are benchmarks for keyword argument calls.
    Brief results when compiled without optimization:
    
      def kw(a: 1) a end
      def kws(**kw) kw end
      h = {a: 1}
    
      kw(a: 1)       # about same
      kw(**h)        # 2.37x faster
      kws(a: 1)      # 1.30x faster
      kws(**h)       # 2.19x faster
      kw(a: 1, **h)  # 1.03x slower
      kw(**h, **h)   # about same
      kws(a: 1, **h) # 1.16x faster
      kws(**h, **h)  # 1.14x faster
Loading