2013-10-08 103 views
0

我剖析我的类库和优化的事情,当我发现这个奇怪的问题:性能损失在基类中访问属性(C#)

有一个基类,我有其他类,源自它。 基类有一个公共属性。 我正在做代码中其他地方涉及此属性的Linq查询。我可以看到,如果我还向派生类中添加了一个具有相同名称的属性(智能感知使用工具提示高亮显示它“此属性隐藏了继承的成员”),因此,可以执行100,000次迭代(甚至不是一百万次)基本上是一个'快捷方式'(但也是财产的重复) - 代码运行速度明显更快...对我来说350毫秒。在100,000次迭代中是非常重要的。

请问为什么? :)可以做什么?


更多细节:

基类:

public abstract class ContentItem: IContent 
{ 
    internal ContentItem() { } 

    [DataMember] 
    [IndexedField(true, false)] 
    public string Guid { get; set; } 

    [DataMember] 
    [IndexedField(false, true)] 
    public string Title { get; set; } 
} 

派生 “POCO”:

[IndexedClass] 
public class Channel : ContentItem, IContent 
{ 
    [DataMember(IsRequired = false, EmitDefaultValue = false)] 
    [ContentField] 
    public string TitleShort { get; set; } 
} 

仓储类(做LINQ查询):(通用库)

public virtual T ByTitle(string title) 
{ 
    return All.Find(item => item.Title == title); 
} 

其中AllList<Channel>并且有2700个项目。

代码来进行测试:

private static void test(Content.Repository<Content.Channel> channels) 
     { 
      int iterations = 100000; 

      var watch = System.Diagnostics.Stopwatch.StartNew(); 

      for (int i = 0; i < iterations; i++) 
      { 
       var channel = channels.ByTitle("Unique-Title"); 
      } 

      watch.Stop(); 

      Console.WriteLine("Done in {0} ms.", watch.ElapsedMilliseconds); 
     } 
+8

你能分享代码来重现这一点,以及你用于基准测试的代码吗? – vcsjones

+0

yep,brb,离开办公室,回家 – Denis

+1

您用于基准测试的代码 –

回答

3

如果您检查生成的IL,当您的代码使用派生类的本地属性时,它将生成call而不是callvirt,这只是便宜得多。

这似乎是一个不成熟的优化,除非您处于时间关键循环。

在使用linq构建迭代时,担心callcallvirt性能之间的差异似乎......特别不成熟。

+0

请问您是什么意思的“过早”?你的意思是我现在不应该担心它吗?但我正在准备一个类库(一个cms的'引擎',所以为什么不把它做完美?) – Denis

+0

以技术上有意义的方式定义“完美”。您的系统在负载情况下的性能是否可以通过'callvirt'处理?是否正在改变你的对象模型,以便你必须让本地成员在LINQ查询中调用与代码库质量相比的性能改进?如果你不能指出规格或缺陷,说明速度不够快......也许担心'callvirt'与'call'在这个阶段没有任何关系。 –

+0

是的,这是一个性能与代码质量,我猜在这个特殊情况下。 CMS不打算处理超过50,000个甚至接近100,000个对象的工作。 (不管是'文章'还是'产品'等),但有代码,例如,检查是否已经存在重复标题或不存在。它在对象的每个“保存”中执行......当你有2,000件物品..好吧...... 350毫米对于100,000件物品来说不会是一个大问题,或者我想:)(如果你曾经设法达到那个容量) – Denis

3

当你隐藏属性你让一个非虚拟的呼叫,而不是成员的虚拟呼叫。虚拟调度确实有一定的相关成本,这就是为什么你能够声明非虚拟属性/方法。

也就是说,在大多数应用程序中,与虚拟方法/属性相关的成本根本不是问题。是的,有一个区别,但是在大多数程序的背景下它并没有太大的意义。

+0

看到两个答案者同时用不同的语言表达同一个东西总是很有趣。 –

+0

它们对我来说是免费的,它们都描述虚拟呼叫...... :) – Denis

0

呼叫和callvirt的差异非常小。

我简化了你的代码。请运行它并告诉我们你有什么答案。 我在使用这两个属性方面没有区别。

public abstract class ContentItem 
{ 
    public string Title { get; set; } 
    public string AnotherTitle { get; set; } 
} 

public class Channel : ContentItem 
{ 
    public string AnotherTitle { get; set; } 
} 

private static void Main(string[] args) 
{ 
    var channels = new List<Channel>(); 
    for (int i = 0; i < 3000; i++) 
    { 
     channels.Add(new Channel(){Title = i.ToString(), AnotherTitle = i.ToString()}); 
    } 
    int iterations = 100000; 
    System.Diagnostics.Stopwatch watch; 
    var difs = new List<int>(); 
    int rounds = 10; 
    for (int k = 0; k < rounds ; k++) 
    { 
     watch = System.Diagnostics.Stopwatch.StartNew(); 
     for (int i = 0; i < iterations; i++) 
     { 
      var channel = channels.Find(item => item.Title == "2345"); 
     } 
     watch.Stop(); 
     long timerValue = watch.ElapsedMilliseconds; 
     watch = System.Diagnostics.Stopwatch.StartNew(); 
     for (int i = 0; i < iterations; i++) 
     { 
      var channel = channels.Find(item => item.AnotherTitle == "2345"); 
     } 
     watch.Stop(); 
     difs.Add((int)(timerValue - watch.ElapsedMilliseconds)); 
    } 

    Console.WriteLine("result middle dif " + difs.Sum()/rounds); 
} 

更新

此外,在这种情况下,你没有在任何IL方法call。 都TitleAnotherTitle看起来像

IL_0008: callvirt instance string ConsoleApplication4.Program/ContentItem::get_Title() 
IL_0016: callvirt instance string ConsoleApplication4.Program/Channel::get_AnotherTitle() 

你无关,与callcallvirt的问题。可能不同之处在于代码,你不会向我们展示。

+0

嗯,有趣。我对get_AnotherTitle编译为callvirt而不是调用感到惊讶。 (自从我在考虑这个问题以来,我一直在考虑永不言弃的虚构调用构造函数的建议) –

+0

结果:原始代码=“中间差5” 然后我删除了“AnotherTitle”从“Channel”类(现在只在基类中),结果是不同-61 ... – Denis