-
-
Notifications
You must be signed in to change notification settings - Fork 103
Enable enqueuing multiple items to Async::Queue #81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
lib/async/queue.rb
Outdated
|
|
||
| def enqueue(item) | ||
| @items.push(item) | ||
| def enqueue(*items) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please check if this allocates a new array? I like this interface, but I just want to confirm we aren't duplicating the argument by doing this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, a single new Array is allocated here. My assumption is new array is used as a local variable inside the method.
From what I can see method arguments (MyObject instances in the script below) are not duplicated.
Do you think this is a problem?
Here's the script I used to test this:
require 'async'
require_relative 'lib/async/queue'
require 'memory'
class MyObject
end
Async do
array = Array.new(3) { MyObject.new }
queue = Async::Queue.new
Memory.report {
queue.enqueue(*array)
}.print
endHere are the results:
# Memory Profile
- Total Allocated: (7.00 B in 1 allocations)
- Total Retained: (0 B in 0 allocations)
## By Gem (40.00 B in 1 allocations)
- (40.00 B in 1 allocations) other
## By File (40.00 B in 1 allocations)
- (40.00 B in 1 allocations) test.rb
## By Location (40.00 B in 1 allocations)
- (40.00 B in 1 allocations) test.rb:12
## By Class (40.00 B in 1 allocations)
- (40.00 B in 1 allocations) Array
## Strings By Gem
## Strings By Location
|
Also, should we consider a way to bulk dequeue items? |
|
i.e. perhaps a similar interface to |
Hmm, a tricky scenario is: a queue has 5 elements, user dequeues 10 elements - what happens? Just like I don't have an immediate need for this, but I can get involved if you think we should explore this feature. |
|
There is one other option... I know the ergonomics isn't as good. But I prefer avoiding |
cf634c4 to
3e01ce5
Compare
|
@ioquatix here's a follow up on our slack discussion. TL;DR:
In the end I went with Alternatively, I think we could go with the naive implementation What are your thoughts? Follow up tasks
require 'async'
require_relative '../lib/async/queue'
require 'benchmark/ips'
class Async::LimitedQueue
# Naive solution.
def enqueue0(*items)
items.each do |item|
self.<<(item)
end
end
def enqueue1(*items)
while !items.empty?
while limited?
@full.wait
end
available = @limit - @items.size
# One array allocation per cycle.
@items.concat(items.shift(available))
self.signal unless self.empty?
end
end
# Optimizes best case scenario with Array#concat
def enqueue2(*items)
available = @limit - @items.size
if available > 0 && available >= items.size
@items.concat(items)
self.signal unless self.empty?
else
items.each do |item|
self.<<(item)
end
end
end
def enqueue3(*items)
available = @limit - @items.size
if available > 0 && available >= items.size
# optimizes best case scenario with Array#concat
@items.concat(items)
self.signal unless self.empty?
else
# First addition is always bulk.
# Three arrays are allocated in this block: another 'enqueue3' call,
# 'items.slice' and 'items.drop'.
enqueue3(items.slice(0, available))
items.drop(available).each do |item|
self.<<(item)
end
end
end
def enqueue4(*items)
available = @limit - @items.size
if available > 0 && available >= items.size
# optimizes best case scenario with Array#concat
@items.concat(items)
self.signal unless self.empty?
else
# same approach as in #enqueue1
while !items.empty?
while limited?
@full.wait
end
available = @limit - @items.size
# One array allocation per cycle.
@items.concat(items.shift(available))
self.signal unless self.empty?
end
end
end
end
Async do |task|
ARRAY_SIZE = 100_000
LIMIT = 99_999
Benchmark.ips do |benchmark|
array = Array.new(ARRAY_SIZE) { rand(10) }
benchmark.report("#enqueue0") do |count|
queue = Async::LimitedQueue.new(LIMIT)
task.async do
queue.enqueue0(*array)
end
task.async do
ARRAY_SIZE.times { queue.dequeue }
end
end
benchmark.report("#enqueue1") do |count|
queue = Async::LimitedQueue.new(LIMIT)
task.async do
queue.enqueue1(*array)
end
task.async do
ARRAY_SIZE.times { queue.dequeue }
end
end
benchmark.report("#enqueue2") do |count|
queue = Async::LimitedQueue.new(LIMIT)
task.async do
queue.enqueue2(*array)
end
task.async do
ARRAY_SIZE.times { queue.dequeue }
end
end
benchmark.report("#enqueue3") do |count|
queue = Async::LimitedQueue.new(LIMIT)
task.async do
queue.enqueue3(*array)
end
task.async do
ARRAY_SIZE.times { queue.dequeue }
end
end
benchmark.report("#enqueue4") do |count|
queue = Async::LimitedQueue.new(LIMIT)
task.async do
queue.enqueue4(*array)
end
task.async do
ARRAY_SIZE.times { queue.dequeue }
end
end
benchmark.compare!
end
endBenchmark results: |
3e01ce5 to
d2685ff
Compare
|
Sorry for taking so long to process this PR. I'll also backport it to Async 1.x |
Co-authored-by: Samuel Williams <samuel.williams@oriontransfer.co.nz>
This PR adds a new feature (and a performance improvement) that enables enqueuing multiple items to
Async::Queue.Use case: I'm using
Async::Queueand adding ~10.000 new items to it at a time.Types of Changes
Testing
Benchmark
TL;DR: this feature is more than 10x faster.
Results: