Граф коммитов

61 Коммитов

Автор SHA1 Сообщение Дата
Peter Zhu 71babe5536 Use 32-bit integers for redblack_id_t
On 32-bit systems, the shape cache size is 1048576 (value of
REDBLACK_CACHE_SIZE), but a 16-bit unsigned integer can only go up to
65536. This means that the redblack_id_t can overflow and lead to a
corrupted red-black tree.

The following script crashes on 32-bit systems:

    o = Object.new
    1_000_000.times do |i|
      o.instance_variable_set(:"@i#{i}", i)
    end
2023-12-04 13:57:12 -05:00
Aaron Patterson 6fce8c7980 Don't try compacting ivars on Classes that are "too complex"
Too complex classes use a hash table to store ivs, and should always pin
their IVs.  We shouldn't touch those classes in compaction.
2023-11-20 16:09:48 -08:00
Jean Boussier 94c9f16663 Refactor rb_obj_evacuate_ivs_to_hash_table
That function is a bit too low level to called from multiple
places. It's always used in tandem with `rb_shape_set_too_complex`
and both have to know how the object is laid out to update the
`iv_ptr`.

So instead we can provide two higher level function:

  - `rb_obj_copy_ivs_to_hash_table` to prepare a `st_table` from an
    arbitrary oject.
  - `rb_obj_convert_to_too_complex` to assign the new `st_table`
    to the old object, and safely free the old `iv_ptr`.

Unfortunately both can't be combined into one, because `rb_obj_copy_ivar`
need `rb_obj_copy_ivs_to_hash_table` to copy from one object
to another.
2023-11-17 09:19:21 +01:00
Peter Zhu 68869e9bd9 Revert "Revert "Remove SHAPE_CAPACITY_CHANGE shapes""
This reverts commit 5f3fb4f4e3.
2023-11-13 18:26:36 -05:00
Peter Zhu 5f3fb4f4e3 Revert "Remove SHAPE_CAPACITY_CHANGE shapes"
This reverts commit f6910a6112.

We're seeing crashes in the test suite of Shopify's core monolith after
this change.
2023-11-10 11:27:49 -05:00
Peter Zhu f6910a6112 Remove SHAPE_CAPACITY_CHANGE shapes
We don't need to create a shape to transition capacity as we can
transition the capacity when the capacity of the SHAPE_IVAR changes.
2023-11-09 09:25:02 -05:00
Jean Boussier d898e8d6f8 Refactor rb_shape_transition_shape_capa out
Right now the `rb_shape_get_next` shape caller need to
first check if there is capacity left, and if not call
`rb_shape_transition_shape_capa` before it can call `rb_shape_get_next`.

And on each of these it needs to checks if we got a TOO_COMPLEX
back.

All this logic is duplicated in the interpreter, YJIT and RJIT.

Instead we can have `rb_shape_get_next` do the capacity transition
when needed. The caller can compare the old and new shapes capacity
to know if resizing is needed. It also can check for TOO_COMPLEX
only once.
2023-11-08 11:02:55 +01:00
Jean Boussier b92b9e1e9e vm_getivar: assume the cached shape_id like have a common ancestor
When an inline cache misses, it is very likely that the stale shape_id
and the current instance shape_id have a close common ancestor.

For example if the instance variable is sometimes frozen sometimes
not, one of the two shape will be the direct parent of the other.

Another pattern that commonly cause IC misses is "memoization",
in such case the object will have a "base common shape" and then
a number of close descendants.

In addition, when we find a common ancestor, we store it in the
inline cache instead of the current shape. This help prevent the
cache from flip-flopping, ensuring the next lookup will be marginally
faster and more generally avoid writing in memory too much.

However, now that shapes have an ancestors index, we only check
for a few ancestors before falling back to use the index.

So overall this change speeds up what is assumed to be the more common
case, but makes what is assumed to be the less common case a bit slower.

```
compare-ruby: ruby 3.3.0dev (2023-10-26T05:30:17Z master 701ca070b4) [arm64-darwin22]
built-ruby: ruby 3.3.0dev (2023-10-26T09:25:09Z shapes_double_sear.. a723a85235) [arm64-darwin22]
warming up......

|                                     |compare-ruby|built-ruby|
|:------------------------------------|-----------:|---------:|
|vm_ivar_stable_shape                 |     11.672M|   11.679M|
|                                     |           -|     1.00x|
|vm_ivar_memoize_unstable_shape       |      7.551M|   10.506M|
|                                     |           -|     1.39x|
|vm_ivar_memoize_unstable_shape_miss  |     11.591M|   11.624M|
|                                     |           -|     1.00x|
|vm_ivar_unstable_undef               |      9.037M|    7.981M|
|                                     |       1.13x|         -|
|vm_ivar_divergent_shape              |      8.034M|    6.657M|
|                                     |       1.21x|         -|
|vm_ivar_divergent_shape_imbalanced   |     10.471M|    9.231M|
|                                     |       1.13x|         -|
```

Co-Authored-By: John Hawthorn <john@hawthorn.email>
2023-11-03 12:47:43 +01:00
Peter Zhu 38ba040d8b Make every initial size pool shape a root shape
This commit makes every initial size pool shape a root shape and assigns
it a capacity of 0.
2023-11-02 13:42:11 -04:00
Jean Boussier b77148ae9f remove_instance_variable: Handle running out of shapes
`remove_shape_recursive` wasn't considering that if we run out of
shapes, it might have to transition to SHAPE_TOO_COMPLEX.

When this happens, we now return with an error and the caller
initiates the evacuation.
2023-11-01 15:21:55 +01:00
Jean Boussier 8e62596e38
Move some defines from shape.h to shape.c
If they are only used there, we might as well not expose them.
2023-10-26 13:07:08 -07:00
Aaron Patterson d8cb827f39 Remove SHAPE_MAX_NUM_IVS
There is no longer a limit on the number of IVs you can store.
SHAPE_MAX_NUM_IVS was used to work around the IV10K problem (the well
known problem where setting 10k instance variables in a row would be too
slow).  The redblack tree works well at any shape depth, even depths
greater than 80, and solves the IV10K problem.
2023-10-24 14:23:17 -07:00
Aaron Patterson afae8df373 `get_next_shape_internal` should always return a shape
If it runs out of shapes, or new variations aren't allowed, it will
return "too complex"
2023-10-24 14:23:17 -07:00
Aaron Patterson a3f66e09f6 geniv objects can become too complex 2023-10-24 10:52:06 -07:00
Aaron Patterson caf6a72348 remove IV limit / support complex shapes on classes 2023-10-24 10:52:06 -07:00
Aaron Patterson 27c7531939 increase the maximum number of ivs 2023-10-24 10:52:06 -07:00
Aaron Patterson 84e4453436 Use a functional red-black tree for indexing the shapes
This is an experimental commit that uses a functional red-black tree to
create an index of the ancestor shapes.  It uses an Okasaki style
functional red black tree:

  https://www.cs.tufts.edu/comp/150FP/archive/chris-okasaki/redblack99.pdf

This tree is advantageous because:

* It offers O(n log n) insertions and O(n log n) lookups.
* It shares memory with previous "versions" of the tree

When we insert a node in the tree, only the parts of the tree that need
to be rebalanced are newly allocated.  Parts of the tree that don't need
to be rebalanced are not reallocated, so "new trees" are able to share
memory with old trees.  This is in contrast to a sorted set where we
would have to duplicate the set, and also resort the set on each
insertion.

I've added a new stat to RubyVM.stat so we can understand how the red
black tree increases.
2023-10-24 10:52:06 -07:00
Katherine Oelsner a7032b80af Revert "shape.h: Make attr_index_t uint8_t"
This reverts commit e3afc212ec.
2023-10-18 15:01:13 -07:00
Jean Boussier e3afc212ec shape.h: Make attr_index_t uint8_t
Given `SHAPE_MAX_NUM_IVS 80`, we transition to TOO_COMPLEX
way before we could overflow a 8bit counter.

This reduce the size of `rb_shape_t` from 32B to 24B.

If we decide to raise `SHAPE_MAX_NUM_IVS` we can always increase
that type again.
2023-10-11 08:33:09 +02:00
Jean Boussier 5cc44f48c5 Refactor rb_shape_transition_shape_capa to not accept capacity
This way the groth factor is encapsulated, which allows
rb_shape_transition_shape_capa to be smarter about ideal sizes.
2023-10-10 14:47:54 +02:00
Peter Zhu 24b137336b Move shape ID to flags for classes on 32 bit
Moves shape ID to FL_USER4 to FL_USER19 for the shape ID on 32 bit
systems. This makes the rb_classext_struct smaller so that it can be
embedded.
2023-04-16 11:06:31 -04:00
Matt Valentine-House d91a82850a Pull the shape tree out of the vm object 2023-04-06 11:07:16 +01:00
Nobuyoshi Nakada 27b1a2992f Adjust SHAPE_BUFFER_SIZE with shape_id_t
On platforms where `shape_id_t` is 16-bits, 0x80000 is out of range of
this type.

```
../src/shape.c: In function ‘shape_alloc’:
../src/shape.c:129:18: warning: comparison is always false due to limited range of data type [-Wtype-limits]
  129 |     if (shape_id == MAX_SHAPE_ID) {
      |                  ^~
```
2023-03-24 13:52:55 -07:00
Aaron Patterson e055c0c716 Make shape functions static
These functions don't need to be in the header file, we can declare them
as static.
2023-03-22 12:50:42 -07:00
Aaron Patterson 1a9e2d20e2 Fix shape allocation limits
We can only allocate enough shapes to fit in the shape buffer.
MAX_SHAPE_ID was based on the theoretical maximum number of shapes we
could have, not on the amount of memory we can actually consume.  This
commit changes the MAX_SHAPE_ID to be based on the amount of memory
we're allowed to consume.

Co-Authored-By: Jemma Issroff <jemmaissroff@gmail.com>
2023-03-22 08:46:12 -07:00
Aaron Patterson 54dbd8bea8 Use an st table for "too complex" objects
st tables will maintain insertion order so we can marshal dump / load
objects with instance variables in the same order they were set on that
particular instance

[ruby-core:112926] [Bug #19535]

Co-Authored-By: Jemma Issroff <jemmaissroff@gmail.com>
2023-03-20 13:54:18 -07:00
Takashi Kokubun 233ddfac54 Stop exporting symbols for MJIT 2023-03-06 21:59:23 -08:00
Takashi Kokubun d579f47558
Bump SHAPE_MAX_NUM_IVS to 80 (#7344) 2023-02-21 10:00:39 -08:00
Jemma Issroff 28da990984 Limit maximum number of IVs on a shape on T_OBJECTS
Create SHAPE_MAX_NUM_IVS (currently 50) and limit all shapes of
T_OBJECTS to that number of IVs. When a shape with a T_OBJECT has more than 50 IVs, fall back to the
obj_too_complex shape which uses hash lookup for ivs.

Note that a previous version of this commit
78fcc9847a was reverted in
88f2b94065 because it did not account for
non-T_OBJECTS
2023-02-06 08:40:51 -08:00
Peter Zhu c4cc3be195 Remove dead code in shapes.c and shapes.h 2023-01-30 14:55:20 -05:00
Aaron Patterson 88f2b94065
Revert "Limit maximum number of IVs on a shape"
This reverts commit 78fcc9847a.
2023-01-26 11:04:55 -05:00
Jemma Issroff 78fcc9847a Limit maximum number of IVs on a shape
Create SHAPE_MAX_NUM_IVS (currently 50) and limit all shapes to that
number of IVs. When a shape has more than 50 IVs, fallback to the
obj_too_complex shape which uses hash lookup for ivs.
2023-01-25 14:48:28 -05:00
Jemma Issroff 66bc620963 Remove unused function `rb_shape_flags_mask` 2023-01-06 11:46:50 -05:00
Takashi Kokubun 1d3bfd804c
MJIT: Export fewer shape functions (#7007) 2022-12-23 10:18:57 -08:00
Peter Zhu c505448cdb Move definition of SIZE_POOL_COUNT back to gc.h
SIZE_POOL_COUNT is a GC macro, it should belong in gc.h and not shape.h.
SIZE_POOL_COUNT doesn't depend on shape.h so we can have shape.h depend
on gc.h.

Co-Authored-By: Matt Valentine-House <matt@eightbitraptor.com>
2022-12-15 16:33:46 -05:00
Matt Valentine-House bfc66e07b7 Fix Object Movement allocation in GC
When moving Objects between size pools we have to assign a new shape.

This happened during updating references - we tried to create a new shape
tree that mirrored the existing tree, but based on the root shape of the
new size pool.

This causes allocations to happen if the new tree doesn't already exist,
potentially triggering a GC, during GC.

This commit changes object movement to look for a pre-existing new tree
during object movement, and if that tree does not exist, we don't move
the object to the new pool.

This allows us to remove the shape allocation from update references.

Co-Authored-By: Peter Zhu <peter@peterzhu.ca>
2022-12-15 15:27:38 -05:00
Jemma Issroff c1ab6ddc9a Transition complex objects to "too complex" shape
When an object becomes "too complex" (in other words it has too many
variations in the shape tree), we transition it to use a "too complex"
shape and use a hash for storing instance variables.

Without this patch, there were rare cases where shape tree growth could
"explode" and cause performance degradation on what would otherwise have
been cached fast paths.

This patch puts a limit on shape tree growth, and gracefully degrades in
the rare case where there could be a factorial growth in the shape tree.

For example:

```ruby
class NG; end

HUGE_NUMBER.times do
  NG.new.instance_variable_set(:"@unique_ivar_#{_1}", 1)
end
```

We consider objects to be "too complex" when the object's class has more
than SHAPE_MAX_VARIATIONS (currently 8) leaf nodes in the shape tree and
the object introduces a new variation (a new leaf node) associated with
that class.

For example, new variations on instances of the following class would be
considered "too complex" because those instances create more than 8
leaves in the shape tree:

```ruby
class Foo; end
9.times { Foo.new.instance_variable_set(":@uniq_#{_1}", 1) }
```

However, the following class is *not* too complex because it only has
one leaf in the shape tree:

```ruby
class Foo
  def initialize
    @a = @b = @c = @d = @e = @f = @g = @h = @i = nil
  end
end
9.times { Foo.new }
``

This case is rare, so we don't expect this change to impact performance
of most applications, but it needs to be handled.

Co-Authored-By: Aaron Patterson <tenderlove@ruby-lang.org>
2022-12-15 10:06:04 -08:00
Peter Zhu f50aa19da6 Revert "Fix Object Movement allocation in GC"
This reverts commit 9c54466e29.

We're seeing crashes in Shopify CI after this commit.
2022-12-15 12:00:30 -05:00
Matt Valentine-House 9c54466e29 Fix Object Movement allocation in GC
When moving Objects between size pools we have to assign a new shape.

This happened during updating references - we tried to create a new shape
tree that mirrored the existing tree, but based on the root shape of the
new size pool.

This causes allocations to happen if the new tree doesn't already exist,
potentially triggering a GC, during GC.

This commit changes object movement to look for a pre-existing new tree
during object movement, and if that tree does not exist, we don't move
the object to the new pool.

This allows us to remove the shape allocation from update references.

Co-Authored-By: Peter Zhu <peter@peterzhu.ca>
2022-12-15 09:04:30 -05:00
Jean Boussier 73771e4b19 ObjectSpace.dump_all: dump shapes as well
I see several arguments in doing so.

First they use a non trivial amount of memory, so for various memory
profiling/mapping tools it is relevant to have visibility of the space
occupied by shapes.

Then, some pathological code can create a tons of shape, so it is
valuable to have a way to have a way to observe shapes without having
to compile Ruby with `SHAPE_DEBUG=1`.

And additionally it's likely much faster to dump then this way than
to use `RubyVM::Shape`.

There are however a few open questions:

- Shapes can't respect the `since:` argument. Not sure what to do when
  it is provided. Would probably make sense to not dump them.
- Maybe it would make more sense to have a separate `ObjectSpace.dump_shapes`?
- Maybe instead `dump_all` should take a `shapes: false` argument?

Additionally, `ObjectSpace.dump_shapes` is added for the use case of
debugging the evolution of the shape tree.
2022-12-08 18:46:16 +01:00
Aaron Patterson edc7af48ac Stop transitioning to UNDEF when undefining an instance variable
Cases like this:

```ruby
obj = Object.new
loop do
  obj.instance_variable_set(:@foo, 1)
  obj.remove_instance_variable(:@foo)
end
```

can cause us to use many more shapes than we want (and even run out).
This commit changes the code such that when an instance variable is
removed, we'll walk up the shape tree, find the shape, then rebuild any
child nodes that happened to be below the "targetted for removal" IV.

This also requires moving any instance variables so that indexes derived
from the shape tree will work correctly.

Co-Authored-By: Jemma Issroff <jemmaissroff@gmail.com>
Co-authored-by: John Hawthorn <jhawthorn@github.com>
2022-12-07 09:57:11 -08:00
Jemma Issroff 41bacd9b0d Remove unused rb_shape_flag_shift and rb_shape_flag_mask 2022-12-02 12:53:51 -08:00
Jemma Issroff 4c5e89791b Extracted rb_shape_id_offset 2022-12-02 12:53:51 -08:00
Aaron Patterson 17f9bcd7d7 implement IV writes 2022-12-02 12:53:51 -08:00
Nobuyoshi Nakada f28e79caaa
Use consistent style [ci skip] 2022-12-02 23:46:21 +09:00
Peter Zhu 09423876f9 Let SHAPE_BITS take 32 bits on debug builds
The ractor_belonging_id has been moved out of the headers, so object
shapes can take the top 32 bits of the flags on debug builds.
2022-11-21 11:26:26 -05:00
Aaron Patterson 9e067df76b 32 bit comparison on shape id
This commit changes the shape id comparisons to use a 32 bit comparison
rather than 64 bit.  That means we don't need to load the shape id to a
register on x86 machines.

Given the following program:

```ruby
class Foo
  def initialize
    @foo = 1
    @bar = 1
  end

  def read
    [@foo, @bar]
  end
end

foo = Foo.new
foo.read
foo.read
foo.read
foo.read
foo.read

puts RubyVM::YJIT.disasm(Foo.instance_method(:read))
```

The machine code we generated _before_ this change is like this:

```
== BLOCK 1/4, ISEQ RANGE [0,3), 65 bytes ======================
  # getinstancevariable
  0x559a18623023: mov rax, qword ptr [r13 + 0x18]
  # guard object is heap
  0x559a18623027: test al, 7
  0x559a1862302a: jne 0x559a1862502d
  0x559a18623030: cmp rax, 4
  0x559a18623034: jbe 0x559a1862502d
  # guard shape, embedded, and T_OBJECT
  0x559a1862303a: mov rcx, qword ptr [rax]
  0x559a1862303d: movabs r11, 0xffff00000000201f
  0x559a18623047: and rcx, r11
  0x559a1862304a: movabs r11, 0xb000000002001
  0x559a18623054: cmp rcx, r11
  0x559a18623057: jne 0x559a18625046
  0x559a1862305d: mov rax, qword ptr [rax + 0x18]
  0x559a18623061: mov qword ptr [rbx], rax

== BLOCK 2/4, ISEQ RANGE [3,6), 0 bytes =======================
== BLOCK 3/4, ISEQ RANGE [3,6), 47 bytes ======================
  # gen_direct_jmp: fallthrough
  # getinstancevariable
  # regenerate_branch
  # getinstancevariable
  # regenerate_branch
  0x559a18623064: mov rax, qword ptr [r13 + 0x18]
  # guard shape, embedded, and T_OBJECT
  0x559a18623068: mov rcx, qword ptr [rax]
  0x559a1862306b: movabs r11, 0xffff00000000201f
  0x559a18623075: and rcx, r11
  0x559a18623078: movabs r11, 0xb000000002001
  0x559a18623082: cmp rcx, r11
  0x559a18623085: jne 0x559a18625099
  0x559a1862308b: mov rax, qword ptr [rax + 0x20]
  0x559a1862308f: mov qword ptr [rbx + 8], rax
```

After this change, it's like this:

```
== BLOCK 1/4, ISEQ RANGE [0,3), 41 bytes ======================
  # getinstancevariable
  0x5560c986d023: mov rax, qword ptr [r13 + 0x18]
  # guard object is heap
  0x5560c986d027: test al, 7
  0x5560c986d02a: jne 0x5560c986f02d
  0x5560c986d030: cmp rax, 4
  0x5560c986d034: jbe 0x5560c986f02d
  # guard shape
  0x5560c986d03a: cmp word ptr [rax + 6], 0x19
  0x5560c986d03f: jne 0x5560c986f046
  0x5560c986d045: mov rax, qword ptr [rax + 0x10]
  0x5560c986d049: mov qword ptr [rbx], rax

== BLOCK 2/4, ISEQ RANGE [3,6), 0 bytes =======================
== BLOCK 3/4, ISEQ RANGE [3,6), 23 bytes ======================
  # gen_direct_jmp: fallthrough
  # getinstancevariable
  # regenerate_branch
  # getinstancevariable
  # regenerate_branch
  0x5560c986d04c: mov rax, qword ptr [r13 + 0x18]
  # guard shape
  0x5560c986d050: cmp word ptr [rax + 6], 0x19
  0x5560c986d055: jne 0x5560c986f099
  0x5560c986d05b: mov rax, qword ptr [rax + 0x18]
  0x5560c986d05f: mov qword ptr [rbx + 8], rax
```

The first ivar read is a bit more complex, but the second ivar read is
much simpler.  I think eventually we could teach the context about the
shape, then emit only one shape guard.
2022-11-18 12:04:10 -08:00
Aaron Patterson 6582f34831 rename SHAPE_BITS to SHAPE_ID_NUM_BITS 2022-11-18 12:04:10 -08:00
Aaron Patterson 10788166e7 Differentiate T_OBJECT shapes from other objects
We would like to differentiate types of objects via their shape.  This
commit adds a special T_OBJECT shape when we allocate an instance of
T_OBJECT.  This allows us to avoid testing whether an object is an
instance of a T_OBJECT or not, we can just check the shape.
2022-11-18 08:31:56 -08:00
Peter Zhu 6dd1a5f532 Remove unused function rb_shape_transition_shape 2022-11-14 11:25:41 -05:00