Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

ko1
Copy link
Contributor

@ko1 ko1 commented Nov 17, 2021

Improve performance of setter/getter of Struct with optimized path.

  S = Struct.new(:a, :b)
  s = S.new(1, 2)
  h = {a: 1, b: 2}
  class C
    attr_accessor :a, :b
    def initialize; @a = 1; @b = 2; end
  end
  c = C.new
  require 'ostruct'
  o = OpenStruct.new(h)
benchmark:
  ivar: c.a; c.b
  Struct: s.a; s.b
  Hash: h[:a]; h[:b]
  ostruct: o.a; o.b
  ivar_set: c.a = c.b = nil
  Struct_set: s.a = s.b = nil
  Hash_set: h[:a] = h[:b] = nil
  ostruct_set: o.a = o.b = nil
master: ruby 3.1.0dev (2021-11-17T06:08:46Z master d23b3d9b7d) [x86_64-linux]
modified: ruby 3.1.0dev (2021-11-18T02:01:31Z opt_struct_aref 8f3366754b) [x86_64-linux]
Warming up --------------------------------------
                ivar    33.279M i/s -     33.309M times in 1.000920s (30.05ns/i, 113clocks/i)
              Struct    25.883M i/s -     25.904M times in 1.000804s (38.64ns/i, 146clocks/i)
                Hash    28.235M i/s -     28.318M times in 1.002938s (35.42ns/i, 134clocks/i)
             ostruct     7.813M i/s -      7.896M times in 1.010588s (127.99ns/i, 478clocks/i)
            ivar_set    29.306M i/s -     29.379M times in 1.002495s (34.12ns/i, 129clocks/i)
          Struct_set    17.276M i/s -     17.462M times in 1.010791s (57.88ns/i, 218clocks/i)
            Hash_set    16.574M i/s -     16.667M times in 1.005657s (60.34ns/i, 228clocks/i)
         ostruct_set     5.638M i/s -      5.692M times in 1.009573s (177.35ns/i, 673clocks/i)
Calculating -------------------------------------
                         master    modified
                ivar    60.246M     60.283M i/s -     99.836M times in 1.657155s 1.656131s
              Struct    39.569M     62.914M i/s -     77.649M times in 1.962351s 1.234199s
                Hash    45.743M     46.692M i/s -     84.706M times in 1.851763s 1.814144s
             ostruct     8.615M      8.757M i/s -     23.439M times in 2.720840s 2.676602s
            ivar_set    48.647M     45.752M i/s -     87.918M times in 1.807265s 1.921629s
          Struct_set    17.543M     46.291M i/s -     51.827M times in 2.954289s 1.119597s
            Hash_set    21.276M     21.404M i/s -     49.721M times in 2.336972s 2.322998s
         ostruct_set     6.177M      6.093M i/s -     16.915M times in 2.738419s 2.776225s

Comparison:
                             ivar
            modified:  60282947.4 i/s
              master:  60245692.0 i/s - 1.00x  slower

                           Struct
            modified:  62914175.1 i/s
              master:  39569174.9 i/s - 1.59x  slower

                             Hash
            modified:  46691778.1 i/s
              master:  45743222.5 i/s - 1.02x  slower

                          ostruct
            modified:   8756880.8 i/s
              master:   8614505.7 i/s - 1.02x  slower

                         ivar_set
              master:  48646710.3 i/s
            modified:  45751569.9 i/s - 1.06x  slower

                       Struct_set
            modified:  46291201.4 i/s
              master:  17543131.2 i/s - 2.64x  slower

                         Hash_set
            modified:  21403871.7 i/s
              master:  21275886.1 i/s - 1.01x  slower

                      ostruct_set
              master:   6177075.3 i/s
            modified:   6092956.8 i/s - 1.01x  slower

@ko1 ko1 force-pushed the opt_struct_aref branch 2 times, most recently from 79657dc to 8f33667 Compare November 18, 2021 02:06
@ko1 ko1 changed the title Opt struct aref Optimize Struct's accessors Nov 18, 2021
@ko1 ko1 marked this pull request as ready for review November 18, 2021 02:08
@ko1 ko1 force-pushed the opt_struct_aref branch 2 times, most recently from dfddc6e to a499691 Compare November 18, 2021 04:00
ko1 added 2 commits November 18, 2021 23:59
Now `rb_method_optimized_t optimized` field is added to represent
optimized method type.
Introduce new optimized method type
`OPTIMIZED_METHOD_TYPE_STRUCT_AREF/ASET` with index information.
Comment on lines +169 to +174
if (calling->kw_splat &&
calling->argc > 0 &&
RB_TYPE_P(argv[calling->argc-1], T_HASH) &&
RHASH_EMPTY_P(argv[calling->argc-1])) {
calling->argc--;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be good to comment this to explain what this check is testing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jeremyevans may know.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is testing for empty splatted keyword hashes. If an empty splatted keyword hash are passed, it should be equivalent to not passing keywords at all.

@maximecb
Copy link
Contributor

@XrXr @jhawthorn can you comment on whether this PR increases or decrease the complexity in YJIT, and how easy/hard it would be for us to support this change?

@jhawthorn
Copy link
Member

can you comment on whether this PR increases or decrease the complexity in YJIT, and how easy/hard it would be for us to support this change?

I think it would very slightly increase complexity, since it's adding two new method types, but those method types should be pretty easy to implement. We should also be able to generate better code for these than we currently do (can now read from the memory location instead of calling the old builtin function).

@XrXr
Copy link
Member

XrXr commented Nov 18, 2021

Yeah, we would need to add code to YJIT if we want to support this new method type. The actual logic is similar to getting an ivar, but a bit simpler because structs have a fixed layout. The relevant code is here:

ruby/internal/struct.h

Lines 121 to 144 in ab737b1

static inline const VALUE *
RSTRUCT_CONST_PTR(VALUE st)
{
const struct RStruct *p = RSTRUCT(st);
if (FL_TEST_RAW(st, RSTRUCT_EMBED_LEN_MASK)) {
return p->as.ary;
}
else {
return p->as.heap.ptr;
}
}
static inline void
RSTRUCT_SET(VALUE st, long k, VALUE v)
{
RB_OBJ_WRITE(st, &RSTRUCT_CONST_PTR(st)[k], v);
}
static inline VALUE
RSTRUCT_GET(VALUE st, long k)
{
return RSTRUCT_CONST_PTR(st)[k];
}

So it's a flag test to find the buffer, and then reading at a fixed index.

@maximecb
Copy link
Contributor

Since you guys don't seem concerned about the complexity, we can approve it. Would either of you want to implement the new struct access mechanism?

@ko1 ko1 merged commit 82ea287 into ruby:master Nov 18, 2021
@schneems
Copy link
Contributor

After compiling with these changes I get a warning:

struct.c:267:1: warning: unused function 'struct_pos_num' [-Wunused-function]
struct_pos_num(VALUE s, VALUE idx)
^
1 warning generated.

@paracycle
Copy link
Contributor

This PR seems to have changed the public API of Struct member setter methods.

Before:

S = Struct.new(:foo)
S.instance_method(:foo=).parameters
#=> [[:req, :_]]

After:

S = Struct.new(:foo)
S.instance_method(:foo=).parameters
#=> [[:rest]]

Is this intentional? I find it weird that a setter method looks like it is accepting zero or more arguments instead of a single required argument.

k0kubun added a commit that referenced this pull request Nov 28, 2022
You shouldn't assume bf->compiler is always non-zero. While struct
aref/aset is no longer a builtin function since
#5131, it seems like you could still
load such an iseq binary.

The refactored code fallbacks to compile_insn_default correctly when
bf->compiler is zero.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants