Skip to content
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

Sending to full Channel<Unit>(CONFLATED) is ~10x slower than sending to full Channel<Unit>(1) #4048

Open
ShikaSD opened this issue Feb 19, 2024 · 1 comment

Comments

@ShikaSD
Copy link

ShikaSD commented Feb 19, 2024

Channel<Unit>(CONFLATED) is a pattern frequently used for signalling in coroutine-based environment. In cases when the channel already contains an element (e.g. receiver is delayed), calling send causes Channel to drop the previous element and replace it with the old one. This behavior is expected and matches documentation of Channel.CONFLATED, but is significantly slower than Channel<Unit>(1), which preserves the original object instead. In cases when the channel can only send/receive the same object (Unit === Unit), the default CONFLATED behavior could be optimized to be more efficient.

Running the benchmarks on Pixel 5:

        // make sure we measure sending to already full channel
        channel.trySend(Unit)

        benchmarkRule.measureRepeated {
            channel.trySend(Unit)
        }

Produces the following results:

         // Executed on Pixel 5 (Android 13)
          652   ns           ChannelBenchmark.conflatedChannelSend
           70.8 ns           ChannelBenchmark.normalChannelSend
          559   ns           ChannelBenchmark.conflatedChannelSend_empty
          576   ns           ChannelBenchmark.normalChannelSend_empty

From the results above, sending to empty channel is roughly the same for both Channel(CONFLATED) and Channel(1), but sending to already full channel is ~10x slower.

@qwwdfsad
Copy link
Member

FTR, the benchmark:

@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(value = 1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
open class ChannelBenchmark {

    /**
    Old channels:
    Benchmark                 Mode  Cnt  Score   Error  Units
    ChannelBenchmark.send     avgt    5  4.469 ± 0.085  us/op
    ChannelBenchmark.trySend  avgt    5  4.785 ± 0.050  us/op

    New channels:
    Benchmark                 Mode  Cnt   Score   Error  Units
    ChannelBenchmark.send     avgt    5  17.290 ± 0.116  us/op
    ChannelBenchmark.trySend  avgt    5   2.752 ± 0.036  us/op
     */
    private val conflated = Channel<Unit>(Channel.CONFLATED)
    private val buffered = Channel<Unit>(1)

    @Benchmark
    fun trySend() = runBlocking {
        repeat(1000) {
            buffered.trySend(Unit)
        }
    }

    @Benchmark
    fun send() = runBlocking {
        repeat(1000) {
            conflated.send(Unit)
        }
    }
}

The root cause is clear (in fact, it is properly highlighted in the original report), but the fix is not -- in order to proceed with that, we should repeat what we did in old channels -- provide a dedicated "conflated; buffer = 1" implementation of the channel, which is an additional maintenance burden and a contribution to the library size.

Could you please elaborate on how frequent/often the use-case "conflated channel of Unit to notify the other party"? How performance-sensitive is it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants