Skip to content

Move Time#xmlschema in core and optimize it#11510

Merged
byroot merged 1 commit intoruby:masterfrom
Shopify:native-xmlschema
Sep 5, 2024
Merged

Move Time#xmlschema in core and optimize it#11510
byroot merged 1 commit intoruby:masterfrom
Shopify:native-xmlschema

Conversation

@casperisfine
Copy link
Copy Markdown
Contributor

@casperisfine casperisfine commented Aug 30, 2024

[Feature #20707]

Converting Time into RFC3339 / ISO8601 representation is an significant
hotspot for applications that serialize data in JSON, XML or other formats.

By moving it into core we can optimize it much further than what strftime will
allow.

compare-ruby: ruby 3.4.0dev (2024-08-29T13:11:40Z master 6b08a50a62) +YJIT [arm64-darwin23]
built-ruby: ruby 3.4.0dev (2024-08-30T13:17:32Z native-xmlschema 34041ff71f) +YJIT [arm64-darwin23]
warming up......

|                        |compare-ruby|built-ruby|
|:-----------------------|-----------:|---------:|
|time.xmlschema          |      1.087M|    5.190M|
|                        |           -|     4.78x|
|utc_time.xmlschema      |      1.464M|    6.848M|
|                        |           -|     4.68x|
|time.xmlschema(6)       |    859.960k|    4.646M|
|                        |           -|     5.40x|
|utc_time.xmlschema(6)   |      1.080M|    5.917M|
|                        |           -|     5.48x|
|time.xmlschema(9)       |    893.909k|    4.668M|
|                        |           -|     5.22x|
|utc_time.xmlschema(9)   |      1.056M|    5.707M|
|                        |           -|     5.40x|

@casperisfine casperisfine force-pushed the native-xmlschema branch 3 times, most recently from d0d6579 to b3976d6 Compare September 5, 2024 08:32
@casperisfine casperisfine changed the title Experiment: move Time#xmlschema in core and optimize it Move Time#xmlschema in core and optimize it Sep 5, 2024
[Feature #20707]

Converting Time into RFC3339 / ISO8601 representation is an significant
hotspot for applications that serialize data in JSON, XML or other formats.

By moving it into core we can optimize it much further than what `strftime` will
allow.

```
compare-ruby: ruby 3.4.0dev (2024-08-29T13:11:40Z master 6b08a50) +YJIT [arm64-darwin23]
built-ruby: ruby 3.4.0dev (2024-08-30T13:17:32Z native-xmlschema 34041ff) +YJIT [arm64-darwin23]
warming up......

|                        |compare-ruby|built-ruby|
|:-----------------------|-----------:|---------:|
|time.xmlschema          |      1.087M|    5.190M|
|                        |           -|     4.78x|
|utc_time.xmlschema      |      1.464M|    6.848M|
|                        |           -|     4.68x|
|time.xmlschema(6)       |    859.960k|    4.646M|
|                        |           -|     5.40x|
|utc_time.xmlschema(6)   |      1.080M|    5.917M|
|                        |           -|     5.48x|
|time.xmlschema(9)       |    893.909k|    4.668M|
|                        |           -|     5.22x|
|utc_time.xmlschema(9)   |      1.056M|    5.707M|
|                        |           -|     5.40x|
```
@casperisfine casperisfine marked this pull request as ready for review September 5, 2024 11:07
@byroot byroot merged commit 57e3fc3 into ruby:master Sep 5, 2024
PChambino added a commit to PChambino/jruby that referenced this pull request Jun 3, 2025
Solves: jruby#8476
Related: ruby/ruby#11510

I was looking for something to learn (J)Ruby internals and this one
looked like something within my ability, so here is my stab at it.

Benchmarks: ~5-10x faster than JRuby 10.0.0.1 + ~1-2x faster than CRuby 3.4.3.

The main difference with CRuby is that I make use of getNanos() for the
fraction digits since `subsec` is derived from getNanos() in JRuby, so
we can skip the subsec logic I believe. Similar to `inspect`.

I tried a couple of iterations of a similar implementation to CRuby but
it would always result in worse performance.

I noticed some methods in ConvertBytes that have very similar number / digit
to byte(s) logic, but I ended up making the logic a bit more focused on
xmlschema needs (for performance), so I didn't re-use the existing ones.

Let me know if there are any suggestions / ideas, or feel free to change
it yourself directly.
byroot added a commit to byroot/rails that referenced this pull request Aug 30, 2025
Ref: ruby/ruby#11510

In Ruby 3.4 I optimized `Time#xmlschema`, so that it's now much faster
than the `strftime` `TimeWithZone` was using.

```
ruby 3.5.0dev (2025-08-27T10:41:07Z master 5257e1298c) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
opt-aj-serialize-index
                         1.177M i/100ms
Calculating -------------------------------------
opt-aj-serialize-index
                         13.961M (± 0.5%) i/s   (71.63 ns/i) -     70.594M in   5.056554s

Comparison:
     previous-commit:  1993282.1 i/s
opt-aj-serialize-index: 13961260.3 i/s - 7.00x  faster
```

Bench:
```ruby

require "bundler/inline"
gemfile do
  gem "benchmark-ips"
  gem "rails", path: "."
end

require "benchmark/ips"
require "active_job/railtie"

class TestApp < Rails::Application
  config.load_defaults Rails::VERSION::STRING.to_f
  config.eager_load = false
  config.secret_key_base = "secret_key_base"
  config.active_job.queue_adapter = :test

  config.logger = Logger.new(nil)
end
Rails.application.initialize!

utc_time = Time.zone.now

BRANCH = `git rev-parse --abbrev-ref HEAD`.strip

Benchmark.ips do |x|
  x.report(BRANCH) do
    utc_time.xmlschema
  end

  x.save!("/tmp/bench-twz")

  x.compare!(order: :baseline)
end
```

Since serializing TimeWithZone is a significant part of the Active Job benchmark,
it has an effect there too:

```
ruby 3.5.0dev (2025-08-27T10:41:07Z master 5257e1298c) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
opt-aj-serialize-index
                        20.870k i/100ms
Calculating -------------------------------------
opt-aj-serialize-index
                        213.253k (± 1.9%) i/s    (4.69 μs/i) -      1.085M in   5.090838s

Comparison:
       previous-commit:   188008.3 i/s
opt-aj-serialize-index:   213252.7 i/s - 1.13x  faster
```
byroot added a commit to byroot/rails that referenced this pull request Aug 30, 2025
Ref: ruby/ruby#11510

In Ruby 3.4 I optimized `Time#xmlschema`, so that it's now much faster
than the `strftime` `TimeWithZone` was using.

```
ruby 3.5.0dev (2025-08-27T10:41:07Z master 5257e1298c) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
opt-aj-serialize-index
                         1.177M i/100ms
Calculating -------------------------------------
opt-aj-serialize-index
                         13.961M (± 0.5%) i/s   (71.63 ns/i) -     70.594M in   5.056554s

Comparison:
     previous-commit:  1993282.1 i/s
opt-aj-serialize-index: 13961260.3 i/s - 7.00x  faster
```

Bench:
```ruby

require "bundler/inline"
gemfile do
  gem "benchmark-ips"
  gem "rails", path: "."
end

require "benchmark/ips"
require "active_job/railtie"

class TestApp < Rails::Application
  config.load_defaults Rails::VERSION::STRING.to_f
  config.eager_load = false
  config.secret_key_base = "secret_key_base"
  config.active_job.queue_adapter = :test

  config.logger = Logger.new(nil)
end
Rails.application.initialize!

utc_time = Time.zone.now

BRANCH = `git rev-parse --abbrev-ref HEAD`.strip

Benchmark.ips do |x|
  x.report(BRANCH) do
    utc_time.xmlschema
  end

  x.save!("/tmp/bench-twz")

  x.compare!(order: :baseline)
end
```

Since serializing TimeWithZone is a significant part of the Active Job benchmark,
it has an effect there too:

```
ruby 3.5.0dev (2025-08-27T10:41:07Z master 5257e1298c) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
opt-aj-serialize-index
                        20.870k i/100ms
Calculating -------------------------------------
opt-aj-serialize-index
                        213.253k (± 1.9%) i/s    (4.69 μs/i) -      1.085M in   5.090838s

Comparison:
       previous-commit:   188008.3 i/s
opt-aj-serialize-index:   213252.7 i/s - 1.13x  faster
```
byroot added a commit to byroot/rails that referenced this pull request Aug 30, 2025
Ref: ruby/ruby#11510

In Ruby 3.4 I optimized `Time#xmlschema`, so that it's now much faster
than the `strftime` `TimeWithZone` was using.

```
ruby 3.5.0dev (2025-08-27T10:41:07Z master 5257e1298c) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
opt-aj-serialize-index
                         1.177M i/100ms
Calculating -------------------------------------
opt-aj-serialize-index
                         13.961M (± 0.5%) i/s   (71.63 ns/i) -     70.594M in   5.056554s

Comparison:
     previous-commit:  1993282.1 i/s
opt-aj-serialize-index: 13961260.3 i/s - 7.00x  faster
```

Bench:
```ruby

require "bundler/inline"
gemfile do
  gem "benchmark-ips"
  gem "rails", path: "."
end

require "benchmark/ips"
require "active_job/railtie"

class TestApp < Rails::Application
  config.load_defaults Rails::VERSION::STRING.to_f
  config.eager_load = false
  config.secret_key_base = "secret_key_base"
  config.active_job.queue_adapter = :test

  config.logger = Logger.new(nil)
end
Rails.application.initialize!

utc_time = Time.zone.now

BRANCH = `git rev-parse --abbrev-ref HEAD`.strip

Benchmark.ips do |x|
  x.report(BRANCH) do
    utc_time.xmlschema
  end

  x.save!("/tmp/bench-twz")

  x.compare!(order: :baseline)
end
```

Since serializing TimeWithZone is a significant part of the Active Job benchmark,
it has an effect there too:

```
ruby 3.5.0dev (2025-08-27T10:41:07Z master 5257e1298c) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
opt-aj-serialize-index
                        20.870k i/100ms
Calculating -------------------------------------
opt-aj-serialize-index
                        213.253k (± 1.9%) i/s    (4.69 μs/i) -      1.085M in   5.090838s

Comparison:
       previous-commit:   188008.3 i/s
opt-aj-serialize-index:   213252.7 i/s - 1.13x  faster
```
byroot added a commit to byroot/rails that referenced this pull request Aug 30, 2025
Ref: ruby/ruby#11510

In Ruby 3.4 I optimized `Time#xmlschema`, so that it's now much faster
than the `strftime` `TimeWithZone` was using.

```
ruby 3.5.0dev (2025-08-27T10:41:07Z master 5257e1298c) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
opt-aj-serialize-index
                         1.177M i/100ms
Calculating -------------------------------------
opt-aj-serialize-index
                         13.961M (± 0.5%) i/s   (71.63 ns/i) -     70.594M in   5.056554s

Comparison:
     previous-commit:  1993282.1 i/s
opt-aj-serialize-index: 13961260.3 i/s - 7.00x  faster
```

Bench:
```ruby

require "bundler/inline"
gemfile do
  gem "benchmark-ips"
  gem "rails", path: "."
end

require "benchmark/ips"
require "active_job/railtie"

class TestApp < Rails::Application
  config.load_defaults Rails::VERSION::STRING.to_f
  config.eager_load = false
  config.secret_key_base = "secret_key_base"
  config.active_job.queue_adapter = :test

  config.logger = Logger.new(nil)
end
Rails.application.initialize!

utc_time = Time.zone.now

BRANCH = `git rev-parse --abbrev-ref HEAD`.strip

Benchmark.ips do |x|
  x.report(BRANCH) do
    utc_time.xmlschema
  end

  x.save!("/tmp/bench-twz")

  x.compare!(order: :baseline)
end
```

Since serializing TimeWithZone is a significant part of the Active Job benchmark,
it has an effect there too:

```
ruby 3.5.0dev (2025-08-27T10:41:07Z master 5257e1298c) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
opt-aj-serialize-index
                        20.870k i/100ms
Calculating -------------------------------------
opt-aj-serialize-index
                        213.253k (± 1.9%) i/s    (4.69 μs/i) -      1.085M in   5.090838s

Comparison:
       previous-commit:   188008.3 i/s
opt-aj-serialize-index:   213252.7 i/s - 1.13x  faster
```
byroot added a commit to byroot/rails that referenced this pull request Aug 30, 2025
Ref: ruby/ruby#11510

In Ruby 3.4 I optimized `Time#xmlschema`, so that it's now much faster
than the `strftime` `TimeWithZone` was using.

```
ruby 3.5.0dev (2025-08-27T10:41:07Z master 5257e1298c) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
opt-aj-serialize-index
                         1.177M i/100ms
Calculating -------------------------------------
opt-aj-serialize-index
                         13.961M (± 0.5%) i/s   (71.63 ns/i) -     70.594M in   5.056554s

Comparison:
     previous-commit:  1993282.1 i/s
opt-aj-serialize-index: 13961260.3 i/s - 7.00x  faster
```

Bench:
```ruby

require "bundler/inline"
gemfile do
  gem "benchmark-ips"
  gem "rails", path: "."
end

require "benchmark/ips"
require "active_job/railtie"

class TestApp < Rails::Application
  config.load_defaults Rails::VERSION::STRING.to_f
  config.eager_load = false
  config.secret_key_base = "secret_key_base"
  config.active_job.queue_adapter = :test

  config.logger = Logger.new(nil)
end
Rails.application.initialize!

utc_time = Time.zone.now

BRANCH = `git rev-parse --abbrev-ref HEAD`.strip

Benchmark.ips do |x|
  x.report(BRANCH) do
    utc_time.xmlschema
  end

  x.save!("/tmp/bench-twz")

  x.compare!(order: :baseline)
end
```

Since serializing TimeWithZone is a significant part of the Active Job benchmark,
it has an effect there too:

```
ruby 3.5.0dev (2025-08-27T10:41:07Z master 5257e1298c) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
opt-aj-serialize-index
                        20.870k i/100ms
Calculating -------------------------------------
opt-aj-serialize-index
                        213.253k (± 1.9%) i/s    (4.69 μs/i) -      1.085M in   5.090838s

Comparison:
       previous-commit:   188008.3 i/s
opt-aj-serialize-index:   213252.7 i/s - 1.13x  faster
```
byroot added a commit to byroot/rails that referenced this pull request Aug 30, 2025
Ref: ruby/ruby#11510

In Ruby 3.4 I optimized `Time#xmlschema`, so that it's now much faster
than the `strftime` `TimeWithZone` was using.

```
ruby 3.5.0dev (2025-08-27T10:41:07Z master 5257e1298c) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
opt-aj-serialize-index
                         1.177M i/100ms
Calculating -------------------------------------
opt-aj-serialize-index
                         13.961M (± 0.5%) i/s   (71.63 ns/i) -     70.594M in   5.056554s

Comparison:
     previous-commit:  1993282.1 i/s
opt-aj-serialize-index: 13961260.3 i/s - 7.00x  faster
```

Bench:
```ruby

require "bundler/inline"
gemfile do
  gem "benchmark-ips"
  gem "rails", path: "."
end

require "benchmark/ips"
require "active_job/railtie"

class TestApp < Rails::Application
  config.load_defaults Rails::VERSION::STRING.to_f
  config.eager_load = false
  config.secret_key_base = "secret_key_base"
  config.active_job.queue_adapter = :test

  config.logger = Logger.new(nil)
end
Rails.application.initialize!

utc_time = Time.zone.now

BRANCH = `git rev-parse --abbrev-ref HEAD`.strip

Benchmark.ips do |x|
  x.report(BRANCH) do
    utc_time.xmlschema
  end

  x.save!("/tmp/bench-twz")

  x.compare!(order: :baseline)
end
```

Since serializing TimeWithZone is a significant part of the Active Job benchmark,
it has an effect there too:

```
ruby 3.5.0dev (2025-08-27T10:41:07Z master 5257e1298c) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
opt-aj-serialize-index
                        20.870k i/100ms
Calculating -------------------------------------
opt-aj-serialize-index
                        213.253k (± 1.9%) i/s    (4.69 μs/i) -      1.085M in   5.090838s

Comparison:
       previous-commit:   188008.3 i/s
opt-aj-serialize-index:   213252.7 i/s - 1.13x  faster
```
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.

3 participants