Skip to content

Make fixnum packing configurable and disable ints#9379

Merged
headius merged 2 commits into
jruby:masterfrom
headius:config_fixnums
May 7, 2026
Merged

Make fixnum packing configurable and disable ints#9379
headius merged 2 commits into
jruby:masterfrom
headius:config_fixnums

Conversation

@headius

@headius headius commented Apr 19, 2026

Copy link
Copy Markdown
Member

During benchmarking I realized two things about the split fixnums:

  • IntFixnum and LongFixnum are the same size, due to object alignment (i.e. having Int gains us nothing)
  • Having all three types (Short, Int, and Long) can lead to wide-ranging Integer code becoming tri-morphic, which may interefere with JVM optimizations.

I created this patch to allow enabling and disabling all four widths of fixnum for experimentation, but having only short and long enabled by default.

I this is a WIP and I don't like how it conditionally enables the different widths. The code is far too complex. A better option would be to have fully specialized paths for each combination (possibly generated) and choose one at boot that will never again check a condition.

will land a separate patch for 10.1.0.0 that just hard disables IntFixnum to keep it minimal.

@headius headius added this to the JRuby 10.1.1.0 milestone Apr 19, 2026
headius added a commit to headius/jruby that referenced this pull request Apr 19, 2026
Disable for release of JRuby 10.1.0.0 to avoid polymorphism. Work
after release will make all four widths configurable and reduce the
complexity of this logic.

See jruby#9379
@headius headius mentioned this pull request Apr 19, 2026
headius added 2 commits May 6, 2026 13:03
With or without Lilliput, byte and int fixnums do not pack any
tighter than short or long fixnums, so disable both. Add config
properties to control the use of all three.
The following situations have purpose-built factories, with all
others falling back on generic versions that check configs
repeatedly:

* Long only, with cache
* Long only, without cache
* Long and short, with cache
* Long and short, without cache

Of these, the fastest performance for tight numeric loops still
seems to be "long only. An analysis of the resulting assembly
with AI help confirms my suspicion that widening and narrowing
short values repeatedly ends up costing enough cycles to offset
any gains from having smaller objects for those numeric ranges.
We may want to consider having the compact fixnums be opt-in and
not enabled by default (with "long only with cache" being the
default mode).
@headius

headius commented May 7, 2026

Copy link
Copy Markdown
Member Author

This is complete, but after some extensive benchmarking on MacOS AArch64 the new ShortFixnum may introduce more instruction overhead than the size is worth. The bimorphic nature of having both LongFixnum and ShortFixnum does not appear to impact things much, but the ShortFixnum constantly widening and narrowing its values for reads and initialization of new instances adds enough overhead to a small loop to reduce performance. It also appears to interfere with escape analysis, both on HotSpot and Graal JIT.

Some loop benchmarks follow.

All shorts:

def foo
  i = 0
  while i < 32_000
    i+=1
    j = 0
    while j < 32_000
      j+=1
    end
  end
end
10.times {
  t = Time.now
  foo
  puts Time.now - t
}

Long and short with caching, HotSpot 25:

3.0774749999999997
2.830375
2.738893
2.7603999999999997
2.81643
2.75495
2.75328
2.7494810000000003
2.771321
2.801205

Long only with caching, HotSpot 25 (-Xfixnum.compact=false):

2.679109
2.154972
2.1185169999999998
2.0998430000000003
2.0842650000000003
2.088898
2.155581
2.0820969999999996
2.125191
2.12845

Long only without caching, HotSpot 25 (-Xfixnum.compact=false -Xfixnum.cache=false):

2.613342
2.065781
1.877827
1.8706520000000002
1.856989
1.899999
1.882702
1.8590119999999999
1.9631930000000002
1.873456

Same on HotSpot 26 (escape analysis seems to have improved? Enabling cache degrades this back to 25 speeds.):

3.782794
2.172116
1.229315
1.2248510000000001
1.231879
1.302273
1.234393
1.220123
1.233962
1.229031

Same on GraalVM 25 (escape analysis eliminating most objects):

0.17663399999999999
0.16367500000000001
0.14729299999999998
0.137191
0.142957
0.13831700000000002
0.143365
0.13711500000000001
0.14526699999999998
0.13666

On the other hand, an array of 1M short-ranged values goes from 37,391,992 bytes to 29,391,992 bytes, almost 22% less memory. That savings only applies to short-ranged integers but it still makes this a tough call since most loops will also be in short range.

@headius headius marked this pull request as ready for review May 7, 2026 16:25
@headius headius merged commit e5003a9 into jruby:master May 7, 2026
210 of 211 checks passed
@headius headius deleted the config_fixnums branch May 7, 2026 16:26
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.

1 participant