Reduce memory usage of instance variable table

## Implementation Summary

* Only keys and only values of hash table are contiguous to eliminate
  structure padding.
* Change upper limit of `iv_tbl` size to `UINT16_MAX` (it seems to be
  acceptable in mruby because the total number of classes/modules
  immediately after starting Redmine is 20,000 or less).
* `iv_tbl*` point hash buckets directly.

## Benchmark Summary

Only the results of typical situations on 64-bit Word-boxing are present
here. For more detailed information, including consideration, see below
report (although most of the body is written in Japanese).

* https://shuujii.github.io/mruby-iv-benchmark

### Memory Usage

Lower value is better.

| iv_tbl Size |    Baseline    |       New      |   Factor   |
|------------:|---------------:|---------------:|-----------:|
|           4 |            88B |            52B |   0.59091x |
|          30 |           536B |           388B |   0.72388x |
|         100 |          2072B |          1540B |   0.74324x |
|         200 |          4120B |          3076B |   0.74660x |

Although not mentioned in the above report, the memory usage of `mrbtest`
(full-core gembox) is as follows in the result by Valgrind.

* Baseline: 108,086 allocs, 16,313,122 bytes allocated
* New:       94,273 allocs, 15,875,214 bytes allocated

### Performance

Higher value is better.

#### `mrb_obj_iv_set`

| iv_tbl Size |    Baseline    |       New      |   Factor   |
|------------:|---------------:|---------------:|-----------:|
|           4 |  88.63003M i/s |  92.60611M i/s |   1.04486x |
|          30 |  32.97066M i/s |  25.25095M i/s |   0.76586x |
|         100 |  16.33224M i/s |  22.74998M i/s |   1.39295x |
|         200 |   5.64484M i/s |   6.79949M i/s |   1.20455x |

#### `mrb_obj_iv_get`

| iv_tbl Size |    Baseline    |      New       |   Factor   |
|------------:|---------------:|---------------:|-----------:|
|           4 | 217.58391M i/s | 237.59912M i/s |   1.09199x |
|          30 | 139.56195M i/s | 160.49470M i/s |   1.14999x |
|         100 | 143.09716M i/s | 190.95047M i/s |   1.33441x |
|         200 |  89.75291M i/s | 134.78717M i/s |   1.50176x |

### Binary Size

Lower value is better.

|    File     |    Baseline    |      New       |   Factor   |
|:------------|---------------:|---------------:|-----------:|
| mruby       |       697,520B |       697,520B |   1.00000x |
| libmruby.a  |     1,046,570B |     1,046,682B |   0.99989x |

## Note

The address in `struct RObject::iv` may change after initialization because
`iv_tbl*` points directly to hash buckets. Therefore, the address cannot be
copied and shared when include/prepend. So, when sharing `iv_tbl`, refer to
it via the sharing source class. As a result, the following bug have also
been fixed.

* [An `iv_tbl` is not shared when a class includes or prepends an empty module](https://gist.github.com/shuujii/0ac23fa24b0c55b2c602b534d81e4a95)
This commit is contained in:
KOBAYASHI Shuji
2021-02-03 23:16:09 +09:00
parent d759a73525
commit 98d091436d
5 changed files with 471 additions and 304 deletions
+47 -1
View File
@@ -231,6 +231,18 @@ assert('Module#const_defined?', '15.2.2.4.20') do
assert_true Test4ConstDefined.const_defined?(:Const4Test4ConstDefined)
assert_false Test4ConstDefined.const_defined?(:NotExisting)
assert_wrong_const_name{ Test4ConstDefined.const_defined?(:wrong_name) }
# shared empty iv_tbl (include)
m = Module.new
c = Class.new{include m}
m::CONST = 1
assert_true c.const_defined?(:CONST)
# shared empty iv_tbl (prepend)
m = Module.new
c = Class.new{prepend m}
m::CONST = 1
assert_true c.const_defined?(:CONST)
end
assert('Module#const_get', '15.2.2.4.21') do
@@ -246,6 +258,18 @@ assert('Module#const_get', '15.2.2.4.21') do
assert_uninitialized_const{ Test4ConstGet.const_get(:I_DO_NOT_EXIST) }
assert_uninitialized_const{ Test4ConstGet.const_get("I_DO_NOT_EXIST::ME_NEITHER") }
assert_wrong_const_name{ Test4ConstGet.const_get(:wrong_name) }
# shared empty iv_tbl (include)
m = Module.new
c = Class.new{include m}
m::CONST = 1
assert_equal 1, c.const_get(:CONST)
# shared empty iv_tbl (prepend)
m = Module.new
c = Class.new{prepend m}
m::CONST = 1
assert_equal 1, c.const_get(:CONST)
end
assert('Module#const_set', '15.2.2.4.23') do
@@ -779,7 +803,7 @@ end
assert('module to return the last value') do
m = module M; :m end
assert_equal(m, :m)
assert_equal(:m, m)
end
assert('module to return nil if body is empty') do
@@ -796,3 +820,25 @@ assert('get constant of parent module in singleton class; issue #3568') do
assert_equal("value", actual)
end
assert('shared empty iv_tbl (include)') do
m1 = Module.new
m2 = Module.new{include m1}
c = Class.new{include m2}
m1::CONST1 = 1
assert_equal 1, m2::CONST1
assert_equal 1, c::CONST1
m2::CONST2 = 2
assert_equal 2, c::CONST2
end
assert('shared empty iv_tbl (prepend)') do
m1 = Module.new
m2 = Module.new{prepend m1}
c = Class.new{include m2}
m1::CONST1 = 1
assert_equal 1, m2::CONST1
assert_equal 1, c::CONST1
m2::CONST2 = 2
assert_equal 2, c::CONST2
end