-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Optimize uniqueness filter in Channel.select_impl
#12814
Merged
straight-shoota
merged 2 commits into
crystal-lang:master
from
straight-shoota:refactor/channel-select-uniq
Dec 8, 2022
Merged
Optimize uniqueness filter in Channel.select_impl
#12814
straight-shoota
merged 2 commits into
crystal-lang:master
from
straight-shoota:refactor/channel-select-uniq
Dec 8, 2022
Conversation
This file contains 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
straight-shoota
changed the title
Skip
Optimize uniqueness filter in Dec 1, 2022
uniq!
in Channel.select_impl
Channel.select_impl
straight-shoota
force-pushed
the
refactor/channel-select-uniq
branch
from
December 1, 2022 23:01
2c1d955
to
74384b0
Compare
bcardiff
approved these changes
Dec 3, 2022
Looks great! Any benchmark? Just to get a sense of what's the improvement on each PR. |
@asterite I first thought benchmarks for this wouldn't be easy, but that's not really true.
The ratio is relatively stable for different sizes and ranges between 3x and 5x faster for the new implementation. CHANNELS = Array(Channel(Nil)).new(100) { Channel(Nil).new }
ARY = CHANNELS.map &.receive_select_action
Benchmark.ips do |x|
x.report "non_blocking_select" do
Channel.non_blocking_select(ARY)
end
x.report "non_uniq_select_impl" do
Channel.non_uniq_select_impl(ARY, true)
end
end
class Channel
def self.non_uniq_select_impl(ops : Indexable(Channel::SelectAction), non_blocking)
# Sort the operations by the channel they contain
# This is to avoid deadlocks between concurrent `select` calls
ops_locks = ops
.to_a
.unstable_sort_by!(&.lock_object_id)
each_skip_duplicates(ops_locks, &.lock)
ops.each_with_index do |op, index|
state = op.execute
case state
in .delivered?
each_skip_duplicates(ops_locks, &.unlock)
return index, op.result
in .closed?
each_skip_duplicates(ops_locks, &.unlock)
return index, op.default_result
in .none?
# do nothing
end
end
if non_blocking
each_skip_duplicates(ops_locks, &.unlock)
return ops.size, NotReady.new
end
# Because `channel#close` may clean up a long list, `select_context.try_trigger` may
# be called after the select return. In order to prevent invalid address access,
# the state is allocated in the heap.
shared_state = SelectContextSharedState.new(SelectState::Active)
contexts = ops.map &.create_context_and_wait(shared_state)
each_skip_duplicates(ops_locks, &.unlock)
Crystal::Scheduler.reschedule
contexts.each_with_index do |context, index|
op = ops[index]
op.lock
op.unwait(context)
op.unlock
end
contexts.each_with_index do |context, index|
if context.activated?
return index, ops[index].wait_result(context)
end
end
raise "BUG: Fiber was awaken from select but no action was activated"
end
private def self.each_skip_duplicates(ops_locks)
# Avoid deadlocks from trying to lock the same lock twice.
# `ops_lock` is sorted by `lock_object_id`, so identical onces will be in
# a row and we skip repeats while iterating.
last_lock_id = nil
ops_locks.each do |op|
if op.lock_object_id != last_lock_id
last_lock_id = op.lock_object_id
yield op
end
end
end
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
In
Channel.select_impl
uses a list of locks that need to be locked before running each action and unlocked afterwards. The locks are retrieved from theSelectAction
s (each action doubles as a lock), but some actions share the same lock (indicated bylock_object_id
, which is usuallyChannel
). Locking the same lock twice would result in a deadlock, so duplicates needs to be avoided.This was originally implemented using
uniq!(&.lock_object_id)
.But this method is actually relatively costly. It's
O(N²)
for cross-comparing all items. For larger sizes (> 11
items) it would even allocate aHash
.The locks also need to be iterated in a constant order to avoid deadlock between concurrent
select
actions. Thus they are sorted usingsort_by!(&.lock_object_id)
and actions with identicallock_object_id
s are consequently ordered in a row. This allows to filter repeated ids on iteration with a cursor for tracking the previous id.This filtering is negligible overhead. Iteration happens exactly twice, once for
lock
and once forunlock
. Forunlock
there is probably not even a need to avoid duplicates because it doesn't block. But it's cheap enough to do regardless.This should be a net performance improvement over
uniq!
.(It would certainly also be more efficient to materialize uniqueness with a similar iterative approach after sorting. But since the collection needs to be iterated only twice, it should be much more efficient to filter on the fly and ensure uniqueness while iterating.)
As an additional change,
sort_by!
is replaced byunstable_sort_by!
. There's no need for stable sort characteristics.Ref: #12756