FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

object.c: compare what a Float holds bit for bit under `MRB_NO_BOXING` by takumin · Pull Request #7362 · mruby/mruby · GitHub

/ mruby Public

object.c: compare what a Float holds bit for bit under MRB_NO_BOXING - #7362

Open
takumin wants to merge 1 commit into
mruby:masterfrom
takumin:float-equal-bits
Open

object.c: compare what a Float holds bit for bit under MRB_NO_BOXING#7362
takumin wants to merge 1 commit into
mruby:masterfrom
takumin:float-equal-bits

Conversation

takumin commented Aug 25, 2026
edited by coderabbitai Bot
Loading

Copy link
Copy Markdown
Contributor

Summary

mrb_obj_eq() asks whether two values hold the same thing, and equal? is the one caller that asks for exactly that. For a Float it asked whether they were equal as numbers instead, which is a different question, and it asked it only under MRB_NO_BOXING: the boxed builds compare the mrb_value and so read the representation. The same expression answered one way there and the other way here.

Changes

File What
src/object.c mrb_obj_eq() compares two Floats with memcmp() under MRB_NO_BOXING; <string.h> for it
test/t/float.rb equal? over 0.0 and -0.0, and over a NaN

Where the two questions part

A Float holds two kinds of pair that tell the questions apart. 0.0 and -0.0 are equal as numbers and hold different bits; a NaN holds a NaN and is equal to no number at all, its own operand included. Reading them as numbers, MRB_NO_BOXING called 0.0 and -0.0 one object and said a NaN was not the same object as itself, while the boxings that keep a Float in the value read the bits and answered both the other way.

memcmp() over sizeof(mrb_float) asks the question the boxed builds answer, so equal? reads alike whatever the build stores a Float in. Float#== is untouched, and what a Float is equal to does not move with it: 0.0 == -0.0 is still true, and a NaN is still equal to nothing.

Behaviour

pzero and nzero are built at run time as z + 0.0 and z * -1.0, and nan as z / z, from z = [0.0][0], so that nothing folds them into a single literal. Bold marks where a build differs from CRuby today.

expression word / nan boxing MRB_NO_BOXING this PR CRuby
pzero.equal?(pzero) true true true true
pzero.equal?(nzero) false true false false
pzero == nzero true true true true
nan.equal?(nan) true false true true
nan == nan false false false false

The this PR column is what all three boxings answer after the change. Measured against CRuby 4.0.6.

Speed

MRB_NO_BOXING is the only build the change reaches, so the counts below are from a MRB_NO_BOXING build of the full-core gembox:

z = [0.0][0]
a = z + 1.5
b = z + 1.5
i = 0
while i < 1000000 do a == b; i += 1 end                 # the shortcut, over two Floats

t = 0.0; i = 0
while i < 1000000 do t += i * 0.5; i += 1 end           # Float arithmetic

h = {}; i = 0
while i < 100000 do h[i * 0.5] = i; i += 1 end          # Float-keyed Hash
i = 0
while i < 100000 do h[i * 0.5]; i += 1 end

Instruction counts as callgrind reads them, taken as Ir(2N) - Ir(N) so that startup cancels:

benchmark master this PR
a == b over two Floats, 1M turns 302,004,732 298,004,732 (-1.3%)
t += i * 0.5, 1M turns 377,005,412 377,005,412
Float-keyed Hash, 100k insert + 100k lookup 228,251,313 228,251,415

memcmp() over eight bytes is four instructions fewer per comparison than loading the two Floats and comparing them, so the path equal? and the == shortcut take is slightly shorter than it was. The other two rows are the same code either way.

Size

bin/mruby .text for every build in build_config/ci/gcc-clang.rb:

build master this PR delta
ascii-ctype 1,293,718 1,293,718 0
bintest 1,307,110 1,307,110 0
byte-string 1,271,494 1,271,494 0
cxx_abi 1,334,041 1,334,041 0
full-debug (-O0) 1,913,478 1,913,478 0

None of the five defines MRB_NO_BOXING, so none of them compiles the edit. The build that does is 96 bytes smaller: object.o .text reads 5,288 against master and 5,192 here, and bin/mruby the same 96 less.

Testing

rake test passes for the default build, for build_config/host-nofloat.rb, for every build in build_config/ci/gcc-clang.rb including the C++ ABI one, and for a MRB_NO_BOXING build of the full-core gembox, with no compiler warnings beyond the one -Wmaybe-uninitialized about eq in hash.c that master already emits in a MRB_NO_BOXING build.

Environment

Machine, toolchain and the compile lines these numbers were taken with
OS Ubuntu 24.04.4 LTS
Kernel Linux 7.0.0-30-generic x86_64
CPU AMD Ryzen 9 5950X (16 cores)
C compiler gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
binutils GNU ld (GNU Binutils) 2.47.20260726
valgrind valgrind-3.27.1 (callgrind)
CRuby 4.0.6 (2026-07-14 revision 03b6d3f889)

The instruction counts were taken with a full-core gembox build at MRB_NO_BOXING:

gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_NO_BOXING -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER

The ## Size table was taken with build_config/ci/gcc-clang.rb:

bintest      gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_GC_FIXED_ARENA -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER -DMRB_USE_DEBUG_HOOK
ascii-ctype  gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_USE_ASCII_CTYPE -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER
byte-string  gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER
cxx_abi      gcc -g -O3 -Wall -Wundef -Wwrite-strings -x c++ -std=gnu++03 -DMRB_GC_FIXED_ARENA -DMRB_USE_CXX_EXCEPTION -DMRB_USE_CXX_ABI -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER
full-debug   gcc -std=gnu99 -g -O3 -Wall -Wundef -Werror-implicit-function-declaration -Wwrite-strings -g3 -O0 -DMRB_GC_STRESS -DMRB_USE_DEBUG_HOOK -DMRB_DEBUG -DMRB_USE_BIGINT -DMRB_USE_COMPLEX -DHAVE_MRUBY_ENCODING_GEM -DMRB_UTF8_STRING -DHAVE_MRUBY_IO_GEM -DMRB_USE_RATIONAL -DHAVE_MRUBY_REGEXP_GEM -DMRB_USE_SET -DMRB_USE_TASK_SCHEDULER

cxx_abi compiles mruby as C++ with gcc -x c++ -std=gnu++03; g++ only links. full-debug is the one build at -O0, enable_debug putting -g3 -O0 after the toolchain default of -g -O3.

Summary by CodeRabbit

  • Bug Fixes

    • Updated Float equal? behavior to compare exact object representations.
    • NaN values now preserve identity when aliased.
    • Positive and negative zero are correctly distinguished.
    • Float equal? remains distinct from numeric == comparisons.
  • Documentation

    • Clarified that equal? checks object representation rather than numeric equality.
  • Tests

    • Added coverage for NaN identity, signed zero, and differences between equal? and ==.

`mrb_obj_eq()` is the shortcut every equality goes through, and what it asks of
two values is whether they hold the same thing. For a Float it asked whether
they were equal as numbers instead, which is a different question, and only
under `MRB_NO_BOXING`: the boxed builds compare the `mrb_value` itself and so
read the representation.

The two questions part over -0.0, which holds what 0.0 does not, and over a
NaN, which is equal to no number at all, its own operand included:

```ruby
z = [0.0][0]
(z + 0.0).equal?(z * -1.0)   # word/nan boxing false, MRB_NO_BOXING true
(z + 0.0) == (z * -1.0)      # true everywhere, as it should be

nan = z / z
nan.equal?(nan)              # word/nan boxing true, MRB_NO_BOXING false
```

`equal?` now answers alike whatever the build stores a Float in, which is what
a value being the object it is asks of a boxing. What a Float is equal to is
untouched: `Float#==` is where that question is asked, 0.0 and -0.0 are still
equal, and a NaN is still equal to nothing.
takumin requested a review from matz as a code owner August 25, 2026 14:46

coderabbitai Bot commented Aug 25, 2026
edited
Loading

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info ⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ce1cc99d-4faa-46f7-9c60-43c7a287ceed

📥 Commits

Reviewing files that changed from the base of the PR and between 44ab336 and 2e8b461.

📒 Files selected for processing (2)
  • src/object.c
  • test/t/float.rb

Included review availability: Your plan provides up to 8 included reviews per hour; 4 remain after this review.


📝 Walkthrough

Walkthrough

Float object equality now compares stored float representations. Tests document and verify identity behavior for NaN, positive zero, and negative zero.

Changes

Float identity comparison

Layer / File(s) Summary
Representation-based float equality and validation
src/object.c, test/t/float.rb
mrb_obj_eq uses byte comparison for floats. Tests verify NaN alias identity, distinguish signed zeros under equal?, and contrast equal? with ==.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 2e8b4

The change makes Float#equal? consistently compare stored bits across build modes while leaving numeric equality unchanged, with targeted tests and no actionable merge-blocking risk remaining after normal checks and review.

Suggested reviewers: matz

🚥 Pre-merge checks | ✅ 5 ✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: bitwise Float comparison in object.c under MRB_NO_BOXING.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant


Back | FazBrowse Home | New Git URL