这是一个有趣的问题,但不是因为缺乏效率.事实上,问题的数字表明通道非常有效.写入无界通道涉及:
- 写入内部ConcurrentQueue and
- 唤醒众多可能的读者之一进行通知.
这意味着排队and唤醒读取器只需要比简单地排队进入ConcurrentQueue多66%.这一点都不坏.不幸的是,这个数字具有欺骗性,尤其是在这种情况下,一个任务或ValueTask大于int
个有效负载,并且"功"可以忽略不计.
基准库(如BenchmarkDotNet)多次运行测试,直到获得统计上稳定的样本,其中包括预热和冷却步骤,以考虑JIT、缓存和预热效果.
为了获得基准,我将BenchmarkDotnet与这个基准类一起使用.我忍不住为SingleReader
优化添加了一个参数,该优化假设一次只能有一个读卡器,因此使用了更简单的队列和锁定.
[MemoryDiagnoser]
[ThreadingDiagnoser]
public class QuestionBenchmarks
{
[Params(true, false)] // Arguments can be combined with Params
public bool SingleReader;
static async Task Produce(Channel<int> channel)
{
DateTime start = DateTime.Now;
for (int i = 0; i < 100000000; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
}
static async Task Consume(Channel<int> channel)
{
while (true)
{
try
{
int r = await channel.Reader.ReadAsync();
}
catch (ChannelClosedException) { break; }
}
}
[Benchmark]
public async Task InlinedBoth()
{
var channel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions { SingleReader = SingleReader });
var t1 = Task.Run(async () =>
{
DateTime start = DateTime.Now;
for (int i = 0; i < 100000000; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
});
var t2 = Task.Run(async () =>
{
while (true)
{
try
{
int r = await channel.Reader.ReadAsync();
}
catch (ChannelClosedException) { break; }
}
});
await Task.WhenAll(t1, t2);
}
[Benchmark]
public async Task InlinedProduceOnly()
{
var channel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions { SingleReader = SingleReader });
var t1 = Task.Run(async () =>
{
DateTime start = DateTime.Now;
for (int i = 0; i < 100000000; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
});
await t1;
}
[Benchmark]
public async Task WithMethods()
{
var channel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions { SingleReader = SingleReader });
var producer = Produce(channel);
var consumer = Consume(channel);
await Task.WhenAll(producer, consumer);
}
[Benchmark]
public async Task WithMethodsProduceOnly()
{
var channel = Channel.CreateUnbounded<int>(new UnboundedChannelOptions { SingleReader = SingleReader });
var producer = Produce(channel);
await producer;
}
}
出乎意料的是:
// * Summary *
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22621
Intel Core i7-10850H CPU 2.70GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK=7.0.100-preview.5.22307.18
[Host] : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT
DefaultJob : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT
有价值观
Method |
SingleReader |
Mean |
Error |
StdDev |
Completed Work Items |
Lock Contentions |
Gen 0 |
Gen 1 |
Gen 2 |
Allocated |
InlinedBoth |
False |
4.193 s |
0.0825 s |
0.0772 s |
9675.0000 |
32071.0000 |
- |
- |
- |
265 KB |
InlinedProduceOnly |
False |
3.842 s |
0.0768 s |
0.1654 s |
1.0000 |
- |
4000.0000 |
4000.0000 |
4000.0000 |
786,464 KB |
WithMethods |
False |
6.181 s |
0.1233 s |
0.2027 s |
- |
- |
4000.0000 |
4000.0000 |
4000.0000 |
786,463 KB |
WithMethodsProduceOnly |
False |
3.805 s |
0.0753 s |
0.0837 s |
- |
- |
4000.0000 |
4000.0000 |
4000.0000 |
786,462 KB |
InlinedBoth |
True |
4.342 s |
0.1200 s |
0.3483 s |
84484.0000 |
22848.0000 |
- |
- |
- |
71 KB |
InlinedProduceOnly |
True |
2.990 s |
0.0595 s |
0.0908 s |
1.0000 |
- |
3000.0000 |
3000.0000 |
3000.0000 |
393,230 KB |
WithMethods |
True |
4.158 s |
0.0814 s |
0.1426 s |
- |
- |
3000.0000 |
3000.0000 |
3000.0000 |
393,230 KB |
WithMethodsProduceOnly |
True |
2.879 s |
0.0547 s |
0.0512 s |
- |
- |
3000.0000 |
3000.0000 |
3000.0000 |
393,232 KB |
Completed Work Items
是线程池中完成的任务数.带有方法的基准测试根本不使用线程池.当然他们不会,因为他们不使用任务.跑使用方法的代码不使用多个线程,因此没有锁冲突.与没有生产者的代码相同.
这意味着无法比较基准.即使如此,很明显,使用SingleReader
使用更少的内存
包含100万个项目的整个基准测试耗时28分钟,所以在创建一个包含更少项目的新的、正确的基准测试之前,我将稍等片刻
Global total time: 00:28:23 (1703.72 sec), executed benchmarks: 10