[ruby-core:117746] [Ruby master Bug#20462] Native threads are no longer reused

Issue #20462 has been reported by tenderlovemaking (Aaron Patterson). ---------------------------------------- Bug #20462: Native threads are no longer reused https://bugs.ruby-lang.org/issues/20462 * Author: tenderlovemaking (Aaron Patterson) * Status: Open * Backport: 3.1: UNKNOWN, 3.2: UNKNOWN, 3.3: UNKNOWN ---------------------------------------- Ruby used to reuse native threads in order to amortize the cost of making a pthread. For example this program: ```ruby ntids = 1000.times.map { Thread.new { Thread.current.native_thread_id }.value } p ntids.uniq.length ``` With Ruby 3.2.0, this would return 1. With Ruby 3.3.x, it returns 1000. It means we cannot amortize the cost of a pthread for short lived threads. I was able to bisect this to commit be1bbd5b7d40ad863ab35097765d3754726bbd54. But the change is big so I don't know how to fix it. -- https://bugs.ruby-lang.org/

Issue #20462 has been updated by jhawthorn (John Hawthorn). In a benchmark of thread creation we see that 3.3 is slower than 3.2 ``` jhawthorn@zergling:~ [ruby 3.2.2] $ time ruby --disable-gems -e '100_000.times { Thread.new{}.join }' ruby --disable-gems -e '100_000.times { Thread.new{}.join }' 0.73s user 1.16s system 107% cpu 1.751 total jhawthorn@zergling:~ [ruby 3.3.1] $ time ruby --disable-gems -e '100_000.times { Thread.new{}.join }' ruby --disable-gems -e '100_000.times { Thread.new{}.join }' 0.55s user 1.67s system 70% cpu 3.126 total ``` ---------------------------------------- Bug #20462: Native threads are no longer reused https://bugs.ruby-lang.org/issues/20462#change-108157 * Author: tenderlovemaking (Aaron Patterson) * Status: Open * Backport: 3.1: UNKNOWN, 3.2: UNKNOWN, 3.3: UNKNOWN ---------------------------------------- Ruby used to reuse native threads in order to amortize the cost of making a pthread. For example this program: ```ruby ntids = 1000.times.map { Thread.new { Thread.current.native_thread_id }.value } p ntids.uniq.length ``` With Ruby 3.2.0, this would return 1. With Ruby 3.3.x, it returns 1000. It means we cannot amortize the cost of a pthread for short lived threads. I was able to bisect this to commit be1bbd5b7d40ad863ab35097765d3754726bbd54. But the change is big so I don't know how to fix it. -- https://bugs.ruby-lang.org/

Issue #20462 has been updated by maciej.mensfeld (Maciej Mensfeld). I also believe it can multiple any potential issues with resources leakage. Not sure yet what is causing this but working with a Rails user that sees this:
I wrote a little script that creates 100k threads and can report 3.3.3 allocates way more memory than 3.2.x 3.2.2 - Memory allocated: 49603264 bytes 3.3.3 - Memory allocated: 104926404080 bytes
---------------------------------------- Bug #20462: Native threads are no longer reused https://bugs.ruby-lang.org/issues/20462#change-108844 * Author: tenderlovemaking (Aaron Patterson) * Status: Open * Backport: 3.1: UNKNOWN, 3.2: UNKNOWN, 3.3: UNKNOWN ---------------------------------------- Ruby used to reuse native threads in order to amortize the cost of making a pthread. For example this program: ```ruby ntids = 1000.times.map { Thread.new { Thread.current.native_thread_id }.value } p ntids.uniq.length ``` With Ruby 3.2.0, this would return 1. With Ruby 3.3.x, it returns 1000. It means we cannot amortize the cost of a pthread for short lived threads. I was able to bisect this to commit be1bbd5b7d40ad863ab35097765d3754726bbd54. But the change is big so I don't know how to fix it. -- https://bugs.ruby-lang.org/

Issue #20462 has been updated by byroot (Jean Boussier). Backport changed from 3.1: UNKNOWN, 3.2: UNKNOWN, 3.3: UNKNOWN to 3.1: DONTNEED, 3.2: DONTNEED, 3.3: UNKNOWN be1bbd5b7d40ad863ab35097765d3754726bbd54 very explicitly removes the cache. What's unclear is whether it was intended, or if the intent was to reimplement it differently and it was forgotten. @ko1 could you let us know what the intent was? ---------------------------------------- Bug #20462: Native threads are no longer reused https://bugs.ruby-lang.org/issues/20462#change-108846 * Author: tenderlovemaking (Aaron Patterson) * Status: Open * Backport: 3.1: DONTNEED, 3.2: DONTNEED, 3.3: UNKNOWN ---------------------------------------- Ruby used to reuse native threads in order to amortize the cost of making a pthread. For example this program: ```ruby ntids = 1000.times.map { Thread.new { Thread.current.native_thread_id }.value } p ntids.uniq.length ``` With Ruby 3.2.0, this would return 1. With Ruby 3.3.x, it returns 1000. It means we cannot amortize the cost of a pthread for short lived threads. I was able to bisect this to commit be1bbd5b7d40ad863ab35097765d3754726bbd54. But the change is big so I don't know how to fix it. -- https://bugs.ruby-lang.org/

Issue #20462 has been updated by maciej.mensfeld (Maciej Mensfeld). Not sure if related but here's a benchmark of the impact (written by Gordon Chan): ```ruby #!/usr/bin/env ruby # Disable GC during the benchmark GC.disable # Get the current memory usage mem_before = GC.stat[:malloc_increase_bytes] obj_before = GC.stat[:total_allocated_objects] # Perform the operation you want to benchmark result = 100_000.times { Thread.new{}.join } # Get the new memory usage and object count mem_after = GC.stat[:malloc_increase_bytes] obj_after = GC.stat[:total_allocated_objects] # Re-enable GC GC.enable GC.stress sleep(5) # Calculate the memory and object allocation deltas mem_delta = mem_after - mem_before obj_delta = obj_after - obj_before puts "Memory allocated: #{mem_delta} bytes" puts "Objects allocated: #{obj_delta}" ``` and the difference: ``` # 3.1.3 Memory allocated: 46402080 bytes Objects allocated: 700009 # 3.2.3 Memory allocated: 49603216 bytes Objects allocated: 700009 # 3.3.3 Memory allocated: 104926404048 bytes Objects allocated: 700010 ``` 3.1 and 3.2 are consistent, while 3.3 is as you can see above (2000x). ---------------------------------------- Bug #20462: Native threads are no longer reused https://bugs.ruby-lang.org/issues/20462#change-108857 * Author: tenderlovemaking (Aaron Patterson) * Status: Open * Backport: 3.1: DONTNEED, 3.2: DONTNEED, 3.3: UNKNOWN ---------------------------------------- Ruby used to reuse native threads in order to amortize the cost of making a pthread. For example this program: ```ruby ntids = 1000.times.map { Thread.new { Thread.current.native_thread_id }.value } p ntids.uniq.length ``` With Ruby 3.2.0, this would return 1. With Ruby 3.3.x, it returns 1000. It means we cannot amortize the cost of a pthread for short lived threads. I was able to bisect this to commit be1bbd5b7d40ad863ab35097765d3754726bbd54. But the change is big so I don't know how to fix it. -- https://bugs.ruby-lang.org/

Issue #20462 has been updated by byroot (Jean Boussier).
Not sure if related
It is. The thread cache was to reuse short lived thread and avoid all these allocations etc. Note however that it wasn't enabled on all platforms, so relying on it never really was a good idea. ---------------------------------------- Bug #20462: Native threads are no longer reused https://bugs.ruby-lang.org/issues/20462#change-108861 * Author: tenderlovemaking (Aaron Patterson) * Status: Open * Backport: 3.1: DONTNEED, 3.2: DONTNEED, 3.3: UNKNOWN ---------------------------------------- Ruby used to reuse native threads in order to amortize the cost of making a pthread. For example this program: ```ruby ntids = 1000.times.map { Thread.new { Thread.current.native_thread_id }.value } p ntids.uniq.length ``` With Ruby 3.2.0, this would return 1. With Ruby 3.3.x, it returns 1000. It means we cannot amortize the cost of a pthread for short lived threads. I was able to bisect this to commit be1bbd5b7d40ad863ab35097765d3754726bbd54. But the change is big so I don't know how to fix it. -- https://bugs.ruby-lang.org/

Issue #20462 has been updated by maciej.mensfeld (Maciej Mensfeld). byroot (Jean Boussier) wrote in #note-5:
Not sure if related
It is. The thread cache was to reuse short lived thread and avoid all these allocations etc. Note however that it wasn't enabled on all platforms, so relying on it never really was a good idea.
I myself do not rely on this explicitly. It came up during a deeper investigation related to high memory usage / potential memory leaks of 3.3.x. We've been postponing the deployment of 3.3 due to much higher memory usage than 3.2 and this is one of the things that came up. As of now I am trying to identify short-lived thread usage amongst other Ruby OSS gems. ---------------------------------------- Bug #20462: Native threads are no longer reused https://bugs.ruby-lang.org/issues/20462#change-108863 * Author: tenderlovemaking (Aaron Patterson) * Status: Open * Backport: 3.1: DONTNEED, 3.2: DONTNEED, 3.3: UNKNOWN ---------------------------------------- Ruby used to reuse native threads in order to amortize the cost of making a pthread. For example this program: ```ruby ntids = 1000.times.map { Thread.new { Thread.current.native_thread_id }.value } p ntids.uniq.length ``` With Ruby 3.2.0, this would return 1. With Ruby 3.3.x, it returns 1000. It means we cannot amortize the cost of a pthread for short lived threads. I was able to bisect this to commit be1bbd5b7d40ad863ab35097765d3754726bbd54. But the change is big so I don't know how to fix it. -- https://bugs.ruby-lang.org/

Issue #20462 has been updated by byroot (Jean Boussier). Assignee set to ko1 (Koichi Sasada) @ko1 told me it was removed intentionally, but I don't have much more details, so I'll defer to him on what to do about this issue. ---------------------------------------- Bug #20462: Native threads are no longer reused https://bugs.ruby-lang.org/issues/20462#change-108868 * Author: tenderlovemaking (Aaron Patterson) * Status: Open * Assignee: ko1 (Koichi Sasada) * Backport: 3.1: DONTNEED, 3.2: DONTNEED, 3.3: UNKNOWN ---------------------------------------- Ruby used to reuse native threads in order to amortize the cost of making a pthread. For example this program: ```ruby ntids = 1000.times.map { Thread.new { Thread.current.native_thread_id }.value } p ntids.uniq.length ``` With Ruby 3.2.0, this would return 1. With Ruby 3.3.x, it returns 1000. It means we cannot amortize the cost of a pthread for short lived threads. I was able to bisect this to commit be1bbd5b7d40ad863ab35097765d3754726bbd54. But the change is big so I don't know how to fix it. -- https://bugs.ruby-lang.org/
participants (4)
-
byroot (Jean Boussier)
-
jhawthorn (John Hawthorn)
-
maciej.mensfeld (Maciej Mensfeld)
-
tenderlovemaking (Aaron Patterson)