闲来无事,看到有争论c#事件机制效率的,小小地测试一番~
结论先放这里:
1.订阅者数量较少的情况下和接口列表访问不相伯仲,事件机制访问速度多次比列表快
2.订阅者较多的情况(百万数量级)下,触发事件消耗的时间是接口列表(遍历器)的1.3倍左右
3.接口列表用数组下标方式访问调用蜜汁效率,比遍历器访问快0.6左右;
4.事件有GC,列表在百万级数量没有GC,样本数量足够大的情况列表出现了蜜汁GC= =
5.事件订阅和取消订阅行为很慢,约40倍于列表增删接口;
6.本猿认为:一个正常程序在他每片生命周期中,即使是onUpdate事件,实际调用的函数不会超过2k的,能达到500就已经是比较大的程序了(从做游戏的角度想,一个场景放500个onUpdate订阅者?简直疯了,不论是用事件还是接口列表,被喷出翔是免不了的了)。相比芝麻绿豆的事件性能消耗,业务逻辑和项目管理更加重要(接口列表意味着要额外维护一个接口、一个列表、至少2个函数(add和remove),evnet则是封装完善线程安全)。简单点来说就是这个测试并没有什么用,根据自己项目需要采用适合的观察者模式实现才是正确的。
代码如下(随便写的,结果建议复制到notepad看看) ****本代码是以WarmUp为前提的,因为不这样做波动太大。无可否认jit对代码优化有巨大影响,实际项目环境可能不一样,仅供参考
HiPerfTimer.cs来自网络,已经忘了出处了。
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
Stopwatch sw = new Stopwatch();
HiPerfTimer p=new HiPerfTimer();
int 样本大小 = 10000000;
//data init
List<Counter> counters = new List<Counter>();
for (int i = 样本大小; i >= 0; i--)
{
var c= new Counter();
counters.Add(c);
c.onUpdate();
}
//warmup
foreach(var v in counters)
{
v.onUpdate();
}
Console.WriteLine("样本ok");
Console.ReadKey();
sw.Restart();
//event
sw.Start();
p.Start();
foreach (var v in counters)
{
onUpdate += v.onUpdate;
}
p.Stop();
sw.Stop();
Console.WriteLine($"订阅事件耗时{sw.ElapsedTicks} - {p.Duration}");
sw.Reset();
sw.Start();
p.Start();
onUpdate();
p.Stop();
sw.Stop();
Console.WriteLine($"发送事件消息(不判空)耗时{sw.ElapsedTicks} - {p.Duration}");
sw.Reset();
sw.Start();
p.Start();
onUpdate?.Invoke();
p.Stop();
sw.Stop();
Console.WriteLine($"发送事件消息(判空)耗时{sw.ElapsedTicks} - {p.Duration}");
sw.Reset();
Action tar=counters[657].onUpdate;
p.Start();
onUpdate-=tar;
p.Stop();
Console.WriteLine($"移除一个事件耗时:{p.Duration}");
//lst
List<Counter> counters1 = new List<Counter>();
sw.Start();
p.Start();
foreach (var v in counters)
{
counters1.Add(v);
}
p.Stop();
sw.Stop();
Console.WriteLine($"记录回调耗时{sw.ElapsedTicks} - {p.Duration}");
sw.Reset();
sw.Start();
p.Start();
foreach (var v in counters1)
{
v.onUpdate();
}
p.Stop();
sw.Stop();
Console.WriteLine($"回调耗时{sw.ElapsedTicks} - {p.Duration}");
sw.Reset();
sw.Start();
p.Start();
for(int i = 0; i < 样本大小; i++)
{
counters1[i].onUpdate();
}
p.Stop();
sw.Stop();
Console.WriteLine($"回调耗时(i) {sw.ElapsedTicks} - {p.Duration}");
sw.Reset();
var tar2=counters[457];
p.Start();
counters1.Remove(tar2);
p.Stop();
Console.WriteLine($"移除事件耗时{p.Duration}");
sw.Reset();
var ary = counters.ToArray();
sw.Start();
p.Start();
foreach (var v in ary)
{
v.onUpdate();
}
p.Stop();
sw.Stop();
Console.WriteLine($"数组回调耗时{sw.ElapsedTicks} - {p.Duration}");
sw.Reset();
sw.Start();
p.Start();
for (int i = 0; i < ary.Length; i++)
{
ary[i].onUpdate();
}
p.Stop();
sw.Stop();
Console.WriteLine($"数组回调(i)耗时{sw.ElapsedTicks} - {p.Duration}");
Console.ReadLine();
}
public static event Action onUpdate;
}
class Counter
{
public int i = 0;
public void onUpdate()
{
i++;
}
}
本文通过代码测试分析了C#中事件机制与接口列表在不同订阅者数量下的性能差异。测试结果显示,在少量订阅者情况下,事件机制表现接近接口列表,但在百万数量级时,事件触发耗时约为接口列表的1.3倍。此外,事件机制在订阅和取消订阅操作上明显较慢,且存在GC开销,而接口列表在大规模数据下可能出现意外的GC行为。作者认为在实际项目中,业务逻辑和项目管理往往比微小的性能差异更重要,应根据需求选择合适的观察者模式实现。
1136

被折叠的 条评论
为什么被折叠?



