rbtree-ruby 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.ja.md +17 -6
- data/README.md +17 -6
- data/lib/rbtree/version.rb +1 -1
- data/lib/rbtree.rb +210 -20
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 327862d13e55ed6406023e1d10676e4b5e94541c59400d9205380af08822e4bf
|
|
4
|
+
data.tar.gz: 465e1a514390286bf0165db4dfa9f25ada4cd3d1410b0115b11eba15b03dcf33
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f354523b8fb78b746cf256949887b89184ce1dcd190403a7024949cded391de24ce6f932720a58ae24592b4b453f3901b1896c9904f546aa03fe4b1524698c2
|
|
7
|
+
data.tar.gz: e1219a20ea771f3fe1ca2fc160d737c787a1a0e81cb5389b63f320dcd1d2ac9d2cc71d9df2b9698d73afc98069f76d39285ca8106691f0025f1d5a5fa8b6bf91
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.2] - 2026-01-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Custom Node Allocator**: `RBTree#initialize` now accepts a `node_allocator:` keyword argument, allowing customization of the node pooling strategy.
|
|
12
|
+
- **AutoShrinkNodePool**: Introduced `RBTree::AutoShrinkNodePool` as the default allocator. It automatically releases memory back to the GC when the pool size exceeds the fluctuation range of recent active node counts, preventing memory bloat in long-running processes with variable loads.
|
|
13
|
+
- **Explicit Overwrite**: `overwrite: true` is now the explicit default for `initialize` and `insert`.
|
|
14
|
+
|
|
8
15
|
## [0.3.1] - 2026-01-15
|
|
9
16
|
|
|
10
17
|
### Added
|
|
@@ -191,6 +198,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
191
198
|
- ASCII diagrams for tree rotation operations
|
|
192
199
|
- MIT License (Copyright © 2026 Masahito Suzuki)
|
|
193
200
|
|
|
201
|
+
[0.3.2]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.2
|
|
202
|
+
[0.3.1]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.1
|
|
194
203
|
[0.3.0]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.3.0
|
|
195
204
|
[0.2.3]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.2.3
|
|
196
205
|
[0.2.2]: https://github.com/firelzrd/rbtree-ruby/releases/tag/v0.2.2
|
data/README.ja.md
CHANGED
|
@@ -236,12 +236,6 @@ tree.max(last: true) # => [2, "b"] (最大キーの最後の値)
|
|
|
236
236
|
|
|
237
237
|
全要素のイテレーションはO(n)時間。
|
|
238
238
|
|
|
239
|
-
### メモリ効率
|
|
240
|
-
|
|
241
|
-
RBTreeは内部的な**メモリプール**を使用してノードオブジェクトを再利用:
|
|
242
|
-
- 頻繁な挿入・削除時のGC負荷を大幅に削減
|
|
243
|
-
- 100,000回の循環操作ベンチマークで**GC時間0.0秒**を達成
|
|
244
|
-
|
|
245
239
|
### RBTree vs Hash vs Array
|
|
246
240
|
|
|
247
241
|
順序付き操作と空間的操作において、RBTreeは単に速いだけでなく、全く異なるクラスの性能を発揮。**50万件**でのベンチマーク:
|
|
@@ -254,6 +248,23 @@ RBTreeは内部的な**メモリプール**を使用してノードオブジェ
|
|
|
254
248
|
| **ソート済みイテレーション** | **O(n)** | O(n log n) | **無料** | 常にソート済み vs 明示的な`sort` |
|
|
255
249
|
| **キー検索** | **O(1)** | O(1) | **同等** | **ハイブリッドハッシュインデックスにより、Hashと同等のO(1)検索速度を実現** |
|
|
256
250
|
|
|
251
|
+
### メモリ効率とカスタムアロケータ
|
|
252
|
+
|
|
253
|
+
RBTreeは内部的な**メモリプール**を使用してノードオブジェクトを再利用します。
|
|
254
|
+
- 頻繁な挿入・削除時のGC負荷を大幅に削減します。
|
|
255
|
+
- **自動縮小**: デフォルトの `AutoShrinkNodePool` は、現在の使用量に対してプールが大きくなりすぎた場合、自動的に未使用ノードをRubyのGCに解放します。これにより、負荷が変動する長時間実行アプリケーションでのメモリ肥大化を防ぎます。
|
|
256
|
+
- **カスタマイズ**: プールの動作をカスタマイズしたり、独自のアロケータを指定することができます:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
# 自動縮小パラメータのカスタマイズ
|
|
260
|
+
pool = RBTree::AutoShrinkNodePool.new(
|
|
261
|
+
history_size: 60, # 60秒の履歴
|
|
262
|
+
buffer_factor: 1.5, # 変動幅の50%をバッファとして保持
|
|
263
|
+
reserve_ratio: 0.2 # 常に最大値の20%を予備として保持
|
|
264
|
+
)
|
|
265
|
+
tree = RBTree.new(node_allocator: pool)
|
|
266
|
+
```
|
|
267
|
+
|
|
257
268
|
### RBTreeを使うべき場面
|
|
258
269
|
|
|
259
270
|
✅ **RBTreeが適している場合:**
|
data/README.md
CHANGED
|
@@ -236,12 +236,6 @@ All major operations run in **O(log n)** time:
|
|
|
236
236
|
|
|
237
237
|
Iteration over all elements takes O(n) time.
|
|
238
238
|
|
|
239
|
-
### Memory Efficiency
|
|
240
|
-
|
|
241
|
-
RBTree uses an internal **Memory Pool** to recycle node objects.
|
|
242
|
-
- Significantly reduces Garbage Collection (GC) pressure during frequent insertions and deletions (e.g., in high-throughput queues).
|
|
243
|
-
- In benchmarks with 100,000 cyclic operations, **GC time was 0.0s** compared to significant pauses without pooling.
|
|
244
|
-
|
|
245
239
|
### RBTree vs Hash vs Array (Overwhelming Power)
|
|
246
240
|
|
|
247
241
|
For ordered and spatial operations, RBTree is not just faster—it is in a completely different class. The following benchmarks were conducted with **500,000 items**:
|
|
@@ -254,6 +248,23 @@ For ordered and spatial operations, RBTree is not just faster—it is in a compl
|
|
|
254
248
|
| **Sorted Iteration** | **O(n)** | O(n log n) | **FREE** | Always sorted vs explicit `sort` |
|
|
255
249
|
| **Key Lookup** | **O(1)** | O(1) | **Equal** | **Hybrid Hash Index provides O(1) access like standard Hash** |
|
|
256
250
|
|
|
251
|
+
### Memory Efficiency & Custom Allocators
|
|
252
|
+
|
|
253
|
+
RBTree uses an internal **Memory Pool** to recycle node objects.
|
|
254
|
+
- Significantly reduces Garbage Collection (GC) pressure during frequent insertions and deletions.
|
|
255
|
+
- **Auto-Shrinking**: The default `AutoShrinkNodePool` automatically releases unused nodes back to Ruby's GC when the pool gets too large relative to current usage, preventing memory leaks in long-running applications with fluctuating workloads.
|
|
256
|
+
- **Customization**: You can customize the pool behavior or provide your own allocator:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
# Customize auto-shrink parameters
|
|
260
|
+
pool = RBTree::AutoShrinkNodePool.new(
|
|
261
|
+
history_size: 60, # 1 minute history
|
|
262
|
+
buffer_factor: 1.5, # Keep 50% buffer above fluctuation
|
|
263
|
+
reserve_ratio: 0.2 # Always keep 20% reserve
|
|
264
|
+
)
|
|
265
|
+
tree = RBTree.new(node_allocator: pool)
|
|
266
|
+
```
|
|
267
|
+
|
|
257
268
|
### When to Use RBTree
|
|
258
269
|
|
|
259
270
|
✅ **Use RBTree when you need:**
|
data/lib/rbtree/version.rb
CHANGED
data/lib/rbtree.rb
CHANGED
|
@@ -77,6 +77,7 @@ class RBTree
|
|
|
77
77
|
#
|
|
78
78
|
# @param args [Hash, Array, nil] optional initial data
|
|
79
79
|
# @param overwrite [Boolean] whether to overwrite existing keys (default: true)
|
|
80
|
+
# @param node_allocator [NodeAllocator] allocator instance to use (default: AutoShrinkNodePool.new)
|
|
80
81
|
# @yieldreturn [Object] optional initial data
|
|
81
82
|
# - If a Hash is provided, each key-value pair is inserted into the tree
|
|
82
83
|
# - If an Array is provided, it should contain [key, value] pairs
|
|
@@ -91,7 +92,7 @@ class RBTree
|
|
|
91
92
|
# tree = RBTree.new([[1, 'one'], [2, 'two']])
|
|
92
93
|
# @example Create with overwrite: false
|
|
93
94
|
# tree = RBTree.new([[1, 'one'], [1, 'uno']], overwrite: false)
|
|
94
|
-
def initialize(*args, overwrite: true, &block)
|
|
95
|
+
def initialize(*args, overwrite: true, node_allocator: AutoShrinkNodePool.new, &block)
|
|
95
96
|
@nil_node = Node.new
|
|
96
97
|
@nil_node.color = Node::BLACK
|
|
97
98
|
@nil_node.left = @nil_node
|
|
@@ -99,9 +100,11 @@ class RBTree
|
|
|
99
100
|
@root = @nil_node
|
|
100
101
|
@min_node = @nil_node
|
|
101
102
|
@hash_index = {} # Hash index for O(1) key lookup
|
|
102
|
-
@
|
|
103
|
+
@node_allocator = node_allocator
|
|
103
104
|
@key_count = 0
|
|
104
105
|
|
|
106
|
+
@overwrite = overwrite
|
|
107
|
+
|
|
105
108
|
if args.size > 0 || block_given?
|
|
106
109
|
insert(*args, overwrite: overwrite, &block)
|
|
107
110
|
end
|
|
@@ -303,7 +306,7 @@ class RBTree
|
|
|
303
306
|
# tree.insert({1 => 'one', 2 => 'two'})
|
|
304
307
|
# @example Bulk insert from Array
|
|
305
308
|
# tree.insert([[1, 'one'], [2, 'two']])
|
|
306
|
-
def insert(*args, overwrite:
|
|
309
|
+
def insert(*args, overwrite: @overwrite, &block)
|
|
307
310
|
if args.size == 2
|
|
308
311
|
key, value = args
|
|
309
312
|
insert_entry(key, value, overwrite: overwrite)
|
|
@@ -1326,30 +1329,17 @@ class RBTree
|
|
|
1326
1329
|
# Allocates a new node or recycles one from the pool.
|
|
1327
1330
|
# @return [Node]
|
|
1328
1331
|
def allocate_node(key, value, color, left, right, parent)
|
|
1329
|
-
node = @
|
|
1330
|
-
if node
|
|
1331
|
-
node.key = key
|
|
1332
|
-
node.value = value
|
|
1333
|
-
node.color = color
|
|
1334
|
-
node.left = left
|
|
1335
|
-
node.right = right
|
|
1336
|
-
node.parent = parent
|
|
1337
|
-
node
|
|
1338
|
-
else
|
|
1339
|
-
node = Node.new(key, value, color, left, right, parent)
|
|
1340
|
-
end
|
|
1332
|
+
node = @node_allocator.allocate(key, value, color, left, right, parent)
|
|
1341
1333
|
@key_count += 1
|
|
1342
1334
|
node
|
|
1343
1335
|
end
|
|
1344
1336
|
|
|
1337
|
+
# Releases a node back to the pool.
|
|
1338
|
+
# @param node [Node] the node to release
|
|
1345
1339
|
# Releases a node back to the pool.
|
|
1346
1340
|
# @param node [Node] the node to release
|
|
1347
1341
|
def release_node(node)
|
|
1348
|
-
node
|
|
1349
|
-
node.right = nil
|
|
1350
|
-
node.parent = nil
|
|
1351
|
-
node.value = nil # Help GC
|
|
1352
|
-
@node_pool << node
|
|
1342
|
+
@node_allocator.release(node)
|
|
1353
1343
|
@key_count -= 1
|
|
1354
1344
|
end
|
|
1355
1345
|
|
|
@@ -1790,3 +1780,203 @@ class RBTree::Node
|
|
|
1790
1780
|
# @return [Array(Object, Object)] the key-value pair
|
|
1791
1781
|
def pair = [key, value]
|
|
1792
1782
|
end
|
|
1783
|
+
|
|
1784
|
+
# Allocator for RBTree nodes.
|
|
1785
|
+
#
|
|
1786
|
+
# @api private
|
|
1787
|
+
class RBTree::NodeAllocator
|
|
1788
|
+
# Allocates a new node.
|
|
1789
|
+
#
|
|
1790
|
+
# @param key [Object] the key
|
|
1791
|
+
# @param value [Object] the value
|
|
1792
|
+
# @param color [Boolean] the color (true=red, false=black)
|
|
1793
|
+
# @param left [Node] the left child
|
|
1794
|
+
# @param right [Node] the right child
|
|
1795
|
+
# @param parent [Node] the parent node
|
|
1796
|
+
def allocate(key, value, color, left, right, parent) = RBTree::Node.new(key, value, color, left, right, parent)
|
|
1797
|
+
|
|
1798
|
+
# Releases a node.
|
|
1799
|
+
#
|
|
1800
|
+
# @param node [Node] the node to release
|
|
1801
|
+
def release(node) = nil
|
|
1802
|
+
end
|
|
1803
|
+
|
|
1804
|
+
# Internal node pool for RBTree.
|
|
1805
|
+
#
|
|
1806
|
+
# Manages recycling of Node objects to reduce object allocation overhead.
|
|
1807
|
+
#
|
|
1808
|
+
# @api private
|
|
1809
|
+
class RBTree::NodePool < RBTree::NodeAllocator
|
|
1810
|
+
def initialize
|
|
1811
|
+
@pool = []
|
|
1812
|
+
end
|
|
1813
|
+
|
|
1814
|
+
# Allocates a new node or recycles one from the pool.
|
|
1815
|
+
#
|
|
1816
|
+
# @param key [Object] the key
|
|
1817
|
+
# @param value [Object] the value
|
|
1818
|
+
# @param color [Boolean] the color (true=red, false=black)
|
|
1819
|
+
# @param left [Node] the left child
|
|
1820
|
+
# @param right [Node] the right child
|
|
1821
|
+
# @param parent [Node] the parent node
|
|
1822
|
+
def allocate(key, value, color, left, right, parent)
|
|
1823
|
+
node = @pool.pop
|
|
1824
|
+
if node
|
|
1825
|
+
node.key = key
|
|
1826
|
+
node.value = value
|
|
1827
|
+
node.color = color
|
|
1828
|
+
node.left = left
|
|
1829
|
+
node.right = right
|
|
1830
|
+
node.parent = parent
|
|
1831
|
+
node
|
|
1832
|
+
else
|
|
1833
|
+
super
|
|
1834
|
+
end
|
|
1835
|
+
end
|
|
1836
|
+
|
|
1837
|
+
# Releases a node back to the pool.
|
|
1838
|
+
#
|
|
1839
|
+
# @param node [Node] the node to release
|
|
1840
|
+
def release(node)
|
|
1841
|
+
node.left = node.right = node.parent = node.value = node.key = nil
|
|
1842
|
+
@pool << node
|
|
1843
|
+
end
|
|
1844
|
+
end
|
|
1845
|
+
|
|
1846
|
+
# Internal node pool for RBTree.
|
|
1847
|
+
#
|
|
1848
|
+
# Manages recycling of Node objects to reduce object allocation overhead.
|
|
1849
|
+
# Includes an auto-shrink mechanism to release memory back to GC when
|
|
1850
|
+
# the pool size exceeds the fluctuation range of recent active node count.
|
|
1851
|
+
#
|
|
1852
|
+
# This class can be used to customize the node allocation strategy by passing
|
|
1853
|
+
# an instance to {RBTree#initialize}.
|
|
1854
|
+
class RBTree::AutoShrinkNodePool < RBTree::NodePool
|
|
1855
|
+
# Initializes a new AutoShrinkNodePool.
|
|
1856
|
+
#
|
|
1857
|
+
# @param max_maintenance_interval [Integer] maximum interval between maintenance checks (default: 1000)
|
|
1858
|
+
# @param target_check_interval [Float] target interval in seconds for maintenance checks (default: 1.0)
|
|
1859
|
+
# @param history_size [Integer] duration in seconds to keep history for fluctuation analysis (default: 120)
|
|
1860
|
+
# @param buffer_factor [Float] buffer factor to apply to observed fluctuation (default: 1.25)
|
|
1861
|
+
# @param reserve_ratio [Float] minimum reserve capacity as a ratio of max active nodes (default: 0.1)
|
|
1862
|
+
def initialize(
|
|
1863
|
+
max_maintenance_interval: 1000,
|
|
1864
|
+
target_check_interval: 1.0,
|
|
1865
|
+
history_size: 120,
|
|
1866
|
+
buffer_factor: 1.25,
|
|
1867
|
+
reserve_ratio: 0.1)
|
|
1868
|
+
@pool = []
|
|
1869
|
+
|
|
1870
|
+
@max_maintenance_interval = max_maintenance_interval
|
|
1871
|
+
@target_check_interval = target_check_interval
|
|
1872
|
+
@history_limit = history_size
|
|
1873
|
+
@buffer_ratio = buffer_factor
|
|
1874
|
+
@reserve_ratio = reserve_ratio
|
|
1875
|
+
|
|
1876
|
+
@maintenance_count = 0
|
|
1877
|
+
@check_interval = 1000
|
|
1878
|
+
@check_count = 0
|
|
1879
|
+
@avg_release_rate = nil
|
|
1880
|
+
@last_check_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1881
|
+
|
|
1882
|
+
@active_nodes = 0
|
|
1883
|
+
@global_max_active = 0
|
|
1884
|
+
@global_min_active = 0
|
|
1885
|
+
@max_active_in_interval = 0
|
|
1886
|
+
@min_active_in_interval = 0
|
|
1887
|
+
@history = []
|
|
1888
|
+
@current_target_capacity = Float::INFINITY
|
|
1889
|
+
end
|
|
1890
|
+
|
|
1891
|
+
# Allocates a new node or recycles one from the pool.
|
|
1892
|
+
#
|
|
1893
|
+
# @param key [Object] the key
|
|
1894
|
+
# @param value [Object] the value
|
|
1895
|
+
# @param color [Boolean] the color (true=red, false=black)
|
|
1896
|
+
# @param left [Node] the left child
|
|
1897
|
+
# @param right [Node] the right child
|
|
1898
|
+
# @param parent [Node] the parent node
|
|
1899
|
+
def allocate(key, value, color, left, right, parent)
|
|
1900
|
+
@active_nodes += 1
|
|
1901
|
+
@max_active_in_interval = @active_nodes if @active_nodes > @max_active_in_interval
|
|
1902
|
+
super
|
|
1903
|
+
end
|
|
1904
|
+
|
|
1905
|
+
# Releases a node back to the pool.
|
|
1906
|
+
#
|
|
1907
|
+
# Checks auto-shrink logic to decide whether to keep the node or let it be GC'd.
|
|
1908
|
+
#
|
|
1909
|
+
# @param node [Node] the node to release
|
|
1910
|
+
def release(node)
|
|
1911
|
+
@active_nodes -= 1
|
|
1912
|
+
@min_active_in_interval = @active_nodes if @active_nodes < @min_active_in_interval
|
|
1913
|
+
|
|
1914
|
+
@check_count += 1
|
|
1915
|
+
|
|
1916
|
+
perform_maintenance if @check_count >= @check_interval
|
|
1917
|
+
|
|
1918
|
+
super if @pool.size < @current_target_capacity
|
|
1919
|
+
end
|
|
1920
|
+
|
|
1921
|
+
private
|
|
1922
|
+
|
|
1923
|
+
def perform_maintenance
|
|
1924
|
+
@maintenance_count += 1
|
|
1925
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1926
|
+
elapsed = now - @last_check_time
|
|
1927
|
+
return if elapsed <= 0
|
|
1928
|
+
|
|
1929
|
+
current_rate = @check_count / elapsed
|
|
1930
|
+
|
|
1931
|
+
if @avg_release_rate.nil?
|
|
1932
|
+
@avg_release_rate = current_rate
|
|
1933
|
+
else
|
|
1934
|
+
@avg_release_rate = (@avg_release_rate * 3 + current_rate) / 4
|
|
1935
|
+
end
|
|
1936
|
+
|
|
1937
|
+
@check_interval = [[(@avg_release_rate * @target_check_interval).to_i, 1].max, @max_maintenance_interval].min
|
|
1938
|
+
|
|
1939
|
+
expired_min = false
|
|
1940
|
+
expired_max = false
|
|
1941
|
+
needs_recalc = false
|
|
1942
|
+
|
|
1943
|
+
cutoff_time = now - @history_limit
|
|
1944
|
+
while !@history.empty? && @history.first[0] < cutoff_time
|
|
1945
|
+
if !expired_min && @history.first[1] == @global_min_active
|
|
1946
|
+
expired_min = true
|
|
1947
|
+
needs_recalc = true
|
|
1948
|
+
end
|
|
1949
|
+
if !expired_max && @history.first[2] == @global_max_active
|
|
1950
|
+
expired_max = true
|
|
1951
|
+
needs_recalc = true
|
|
1952
|
+
end
|
|
1953
|
+
@history.shift
|
|
1954
|
+
end
|
|
1955
|
+
|
|
1956
|
+
@history << [now, @min_active_in_interval, @max_active_in_interval]
|
|
1957
|
+
|
|
1958
|
+
if @min_active_in_interval < @global_min_active
|
|
1959
|
+
@global_min_active = @min_active_in_interval
|
|
1960
|
+
expired_min = false
|
|
1961
|
+
needs_recalc = true
|
|
1962
|
+
end
|
|
1963
|
+
if @max_active_in_interval > @global_max_active
|
|
1964
|
+
@global_max_active = @max_active_in_interval
|
|
1965
|
+
expired_max = false
|
|
1966
|
+
needs_recalc = true
|
|
1967
|
+
end
|
|
1968
|
+
|
|
1969
|
+
@global_min_active = @history.map { |_, min, _| min }.min if expired_min
|
|
1970
|
+
@global_max_active = @history.map { |_, _, max| max }.max if expired_max
|
|
1971
|
+
if needs_recalc
|
|
1972
|
+
fluctuation = @global_max_active - @global_min_active
|
|
1973
|
+
reserve = (@reserve_ratio * @global_max_active).to_i
|
|
1974
|
+
@current_target_capacity = [(fluctuation * @buffer_ratio).to_i, reserve].max
|
|
1975
|
+
end
|
|
1976
|
+
|
|
1977
|
+
@check_count = 0
|
|
1978
|
+
@last_check_time = now
|
|
1979
|
+
@max_active_in_interval = @active_nodes
|
|
1980
|
+
@min_active_in_interval = @active_nodes
|
|
1981
|
+
end
|
|
1982
|
+
end
|