我已经遇到一些规则(建议)使用具体的List
和Dictionary
而不是IList
和IDictionary
,因为示例测试显示通过界面访问慢一点例如,向列表中添加10000个值,然后在列表上执行Count
10亿次,这表明通过接口执行此操作比通过具体类执行它慢28倍。即,通过具体类需要80ms,通过界面需要2800ms,这表明它通过界面的速度非常慢。鉴于此,使用具体类是合理的。有没有理由说接口这么慢? (可能更多的是针对那些了解更多关于.net内部的人。)
答案 0 :(得分:8)
我认为如果你看一下反汇编就很明显了:
IList
版本编译为:
for (int i = 0; i < 1000000000; i++)
0000003d xor edi,edi
{
count = lst.Count;
0000003f mov ecx,esi
00000041 call dword ptr ds:[00280024h]
00000047 mov ebx,eax
for (int i = 0; i < 1000000000; i++)
00000049 inc edi
0000004a cmp edi,3B9ACA00h
00000050 jl 0000003F
}
IList.Count
的访问权限被编译为call
指令。
另一方面List
版本内联:
for (int i = 0; i < 1000000000; i++)
0000003a xor edx,edx
0000003c mov eax,dword ptr [esi+0Ch]
0000003f mov esi,eax
00000041 inc edx
00000042 cmp edx,3B9ACA00h
00000048 jl 0000003F
}
这里没有call
指令。只是循环中的mov,inc,cmp和jl指令。当然这更快。
但请记住:通常情况下,你用你的列表内容做事情,你不只是迭代它。这通常比单个函数调用花费的时间长,因此调用接口方法很少会导致任何性能问题。
答案 1 :(得分:6)
使用接口的主要原因是灵活性,关注点分离等。
所以我仍然建议在大多数情况下使用接口(不是全部,只是在适当的地方)并且只考虑在存在实际性能问题时切换到具体类。
并且,在没有GC.Collect()
的情况下运行基准测试并且在发布模式下,我得到96和560毫秒。相差很小。
答案 2 :(得分:6)
您的表现测试显然是错误的。你正在测试很多不同的东西。首先,GC.Collect
会触发终结器线程,这会影响其后运行的所有内容的性能。接下来,您不仅要测试调用接口方法所花费的时间,还要花费大量时间将数据复制到新数组(因为您正在创建大型数组)并收集它们! - 因此接口调用之间存在差异实例调用将完全迷失。
这是一个测试,我测试了接口调用的原始性能开销。在Visual Studio之外的发布模式下运行时:
public interface IMyInterface
{
void InterfaceMethod();
}
public class MyClass : IMyInterface
{
[MethodImpl(MethodImplOptions.NoInlining)]
public void InterfaceMethod()
{
}
[MethodImpl(MethodImplOptions.NoInlining)]
public void InstanceMethod()
{
}
}
class Program
{
static void Main(string[] args)
{
// JITting everyting:
MyClass c = new MyClass();
c.InstanceMethod();
c.InterfaceMethod();
TestInterface(c, 1);
TestConcrete(c, 1);
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
var x = watch.ElapsedMilliseconds;
// Starting tests:
watch = Stopwatch.StartNew();
TestInterface(c, Int32.MaxValue - 2);
var ms = watch.ElapsedMilliseconds;
Console.WriteLine("Interface: " + ms);
watch = Stopwatch.StartNew();
TestConcrete(c, Int32.MaxValue - 2);
ms = watch.ElapsedMilliseconds;
Console.WriteLine("Concrete: " + ms);
}
static void TestInterface(IMyInterface iface, int iterations)
{
for (int i = 0; i < iterations; i++)
{
iface.InterfaceMethod();
}
}
static void TestConcrete(MyClass c, int iterations)
{
for (int i = 0; i < iterations; i++)
{
c.InstanceMethod();
}
}
}
输出:
Interface: 4861
Concrete: 4236
答案 3 :(得分:3)
这在您的应用程序中确实是一个问题,接口可以使您的代码更好,更容易重用/维护
如果你真的需要提高性能,首先尝试改进算法,例如,你真的需要计算10亿次元素吗?你不能把计数存储在某个地方,并有一些标志,表明元素已经改变,你需要重新计算它吗?
据说Performance impact of changing to generic interfaces的问题解决了接口的性能
答案 4 :(得分:3)
未经优化的DEBUG模式没有明显的性能差异,而且使用.Net 3.5,Visual Stusio 2008,RELEASE模式的性能提升了35-40%。
Debug:
List test, ms: 1234.375
IList test, ms: 1218.75
Release:
List test, ms: 609.375
IList test, ms: 968.75
测试代码:
List<int> list = new List<int>();
var start = DateTime.Now;
for (int i = 0; i < 50000000; i++) list.Add(i);
for (int i = 0; i < 50000000; i++) list[i] = 0;
var span = DateTime.Now - start;
Console.WriteLine("List test, ms: {0}", span.TotalMilliseconds);
IList<int> ilist = new List<int>();
start = DateTime.Now;
for (int i = 0; i < 50000000; i++) ilist.Add(i);
for (int i = 0; i < 50000000; i++) ilist[i] = 0;
span = DateTime.Now - start;
Console.WriteLine("IList test, ms: {0}", span.TotalMilliseconds);
答案 5 :(得分:2)
这是我用来查看差异的测试代码:
using System;
using System.Collections.Generic;
using System.Diagnostics;
public class test1
{
static void Main(string[] args)
{
Stopwatch sw = new Stopwatch();
const int NUM_ITEMS = 10000;
const int NUM_LOOPS2 = 1000000000;
List<int> lst = new List<int>(NUM_ITEMS);
IList<int> ilst = lst;
for (int i = 0; i < NUM_ITEMS; i++)
{
lst.Add(i);
}
int count = 0;
sw.Reset();
//GC.Collect();
sw.Start();
for (int i = 0; i < NUM_LOOPS2; i++)
{
count = lst.Count;
}
sw.Stop();
Console.Out.WriteLine("Took " + (sw.ElapsedMilliseconds) + "ms - 1.");
sw.Reset();
//GC.Collect();
sw.Start();
for (int i = 0; i < NUM_LOOPS2; i++)
{
count = ilst.Count;
}
sw.Stop();
Console.Out.WriteLine("Took " + (sw.ElapsedMilliseconds) + "ms - 2.");
}
}
请注意,垃圾收集似乎不会影响此测试。
答案 6 :(得分:0)
here详细说明了IDictionary
比Dictionary
慢的原因。
如果枚举IDictionary
,则确实会产生垃圾,垃圾有时会由GC收集。那就是您看到性能差异的地方。如果您问我,私有成员应始终使用其具体类型进行声明,从而允许编译器至少考虑类中特定于类型的优化(例如枚举)。