从一次重复造轮子中发现的一个性能优化思路。

clark523 2013-10-25 04:25:54
起因是这样的,自己出于各种原因,一方面是兴趣,一方面是目前市面上没有找到比较合适的开源项目。
所以自己重复造轮子写了一个IOC容器。
一开始很简单就是使用Dictionary来进行缓存,使用Reflect进行创建。
在优化过程中,我发现c#中可以用这么一个有趣的写法来进行提升效率。
一开始我的缓存的写法是这样的。

Dictionary<Type, Dictionary<string, RegistObjectContext>()> dic;
if(!dic.ContainsKey(key)){
dic.Add(key, new Dictionary<string, RegistObjectContext>());
}
cDic = dic[key];


而优化后是这样的

try
{
cDic = dic[pType];
}
catch (KeyNotFoundException)
{
dic.Add(pType, new Dictionary<string, RegistObjectContext>());
cDic = dic[pType];
}


在进行100w次运算,优化前和优化后的写法,大概有100-200ms的区别。
为什么会这样呢?
就是因为有些时候我们进行验证所消耗的性能反而高于抛出特定异常所消耗的性能。
这样,我们在有些时候就可以利用这些特定异常来跳过判定过程,以达到提速的目的。
希望能对大家有所帮助
...全文
614 31 打赏 收藏 转发到动态 举报
写回复
用AI写文章
31 条回复
切换为时间正序
请发表友善的回复…
发表回复
clark523 2013-10-28
  • 打赏
  • 举报
回复
引用 28 楼 liuxiaoyi666 的回复:
引用 29 楼 sj178220709 的回复:
@两位来看一下我这个测试能说明问题不。
clark523 2013-10-28
  • 打赏
  • 举报
回复
1楼的测试方案本身也不是为了测试这个的效率而写的,主要是测试其他场景而做。
下面的截图是我根据命中率来重新写的一个测试。

都是执行的1000w次。
在0%命中率的情况下。
tryCatch 要比Normal快3%左右
在50%命中率的情况下。
tryCatch 要比Normal快10%左右
在近似100%命中率的情况下。
tryCatch 要比Normal快35%左右。
当然我这个由于样本比较少,只执行了一次。
有兴趣的同学可以自己跑一下测试。
由此至少我们可以悲观的得到,在0%命中率的情况下,tryCatch不会比Normal效率低。
而效率成长与命中率是成正比的。

测试代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TryCatchTest
{
class Program
{


private static string[] strUnit = { "bytes", "kb", "mb", "gb" };

private static string[] strDateUnit = { "ms", "s", "m", "h" };

private string readCommand()
{
Console.Write("command:");
var command = Console.ReadKey();
Console.WriteLine();
return command.KeyChar.ToString();
}

static void Main(string[] args)
{
Program program = new Program();
Console.WriteLine("Please input howmany times to run");
int count = int.Parse(Console.ReadLine());
Console.WriteLine("Please enter command...");
Console.WriteLine("1. run all new normal");
Console.WriteLine("2. run all new tryCatch");
Console.WriteLine("3. run half new normal");
Console.WriteLine("4. run half new tryCatch");
Console.WriteLine("0. exit");

var command = program.readCommand();
Dictionary<string, string> dic;
try
{
while (command != "0")
{
switch (command)
{
case "1":
dic = new Dictionary<string, string>();
program.run(count, p =>
{
string key = p.ToString();
if (!dic.ContainsKey(key))
{
dic.Add(key, key);
}
else
{
dic[key] = key;
}
}, "all new normal");
break;
case "2":
dic = new Dictionary<string, string>();
program.run(count, p =>
{
string key = p.ToString();
try
{
dic[key] = key;
}
catch (KeyNotFoundException)
{
dic.Add(key, key);
}
}, "all new tryCatch");
break;
case "3":
dic = new Dictionary<string, string>();
program.run(count, p =>
{
string key = (p / 2).ToString();
if (!dic.ContainsKey(key))
{
dic.Add(key, key);
}
else
{
dic[key] = key;
}
}, "half new normal");
break;
case "4":
dic = new Dictionary<string, string>();
program.run(count, p =>
{
string key = (p / 2).ToString();
try
{
dic[key] = key;
}
catch (KeyNotFoundException)
{
dic.Add(key, key);
}
}, "half new tryCatch");
break;
case "5":
dic = new Dictionary<string, string>();
program.run(count, p =>
{
string key = "1";
if (!dic.ContainsKey(key))
{
dic.Add(key, key);
}
else
{
dic[key] = key;
}
}, "half new normal");
break;
case "6":
dic = new Dictionary<string, string>();
program.run(count, p =>
{
string key = "1";
try
{
dic[key] = key;
}
catch (KeyNotFoundException)
{
dic.Add(key, key);
}
}, "1 new tryCatch");
break;
default:
dic = new Dictionary<string, string>();
break;
}
Console.WriteLine("dic length {0}", dic.Count.ToString());
command = program.readCommand();
}
}
catch (OutOfMemoryException ex)
{
Console.WriteLine("create too many instance, please minus the instace count, press enter to quit");
Console.ReadLine();
}
}

private void run(int count, Action<int> func, string methodName)
{
var sDate = DateTime.Now;
var originUse = GC.GetTotalMemory(false);
for (var i = 0; i < count; i++)
{
func(i);
}
var currentUse = GC.GetTotalMemory(false);
var eDate = DateTime.Now;
double use = currentUse - originUse;
var dateDiff = eDate - sDate;

int index = 0;

var unit = strUnit[index];

var dateUnit = strDateUnit[index];

while (Math.Abs(use) > 1024)
{
use = use / 1024;
index++;
unit = strUnit[index];
}

index = 0;

double ms = dateDiff.TotalMilliseconds;

while (ms > 1000)
{
ms = ms / 1000;
index++;
dateUnit = strDateUnit[index];
}

Console.WriteLine("use {1} run{0} times use time {2}{3}", count, methodName, ms, dateUnit);
}
}
}



  • 打赏
  • 举报
回复
引用 26 楼 clark523 的回复:
[quote=引用 25 楼 sj178220709 的回复:] cDic = dic[key]; 这个地方,楼主改一下,再测试看看。 try会额外消耗性能,这是肯定的, 但是你第一种多查询了一次。 至于第二种方法,还要看cache命中率,如果很高,第二种还有点优势,但如果命中率比较低,则异常引发次数太多,肯定不行。 不过归根结底来说,TryGetValue还是最好的,代码更简洁优美,这种内存操作,性能从来不是问题,优化的意义不大。
唉。。。还是没有办法脱离传统思想。 TryGetValue和我的第一种效率是完全一样的。 [/quote] 提个观点,大家探讨下是好的,但是楼主貌似有点小得瑟了啊。 传统之所以是传统,就是因为它的普适性,它确实是人们在生产中总结出来的精华。 你的优化方案,使用try catch这种重型武器,就为了干这点小事,有些过了。 代码量更大,思路更绕是否值得? 而且,好几个人包括我都质疑了你这样做是否能优化性能,恕我直言,你在一楼的测试方案,简直就是粗鄙不堪的。 命中率在这个场景中,是非常重要的一个因素,提出来了,你也不能正面应对。
  • 打赏
  • 举报
回复
cDic = dic[key]; 这个地方,楼主改一下,再测试看看。 try会额外消耗性能,这是肯定的, 但是你第一种多查询了一次。 至于第二种方法,还要看cache命中率,如果很高,第二种还有点优势,但如果命中率比较低,则异常引发次数太多,肯定不行。 不过归根结底来说,TryGetValue还是最好的,代码更简洁优美,这种内存操作,性能从来不是问题,优化的意义不大。
  • 打赏
  • 举报
回复
可以考虑一下你的样本 比如说 你的dic中的数据是123 你要返还的数据的样本是2141 你的两种不同的代码的执行是 第一种 带containskey的执行 判断是否有key为2 赋值key为2 判断是否有key为1 赋值key为1 判断是否有key为4 初始化key为4 赋值key为4 判断是否有key为1 赋值key为1 总共9步 第二种 try的 赋值key为2 赋值key为1 赋值key为4 key为4出错 初始化key为4 赋值key为4 赋值key为1 总共7步 以此为例,你的样本对性能的影响很大,所以暂时看来 论据不足以支持论点 不足以得出try catch比containskey的性能要优化。现在可以得出的结论是,在预知样本命中率显著 高于非命中的时候try catch比containskey方案要优化,并不能绝对保证性能的优化 有待于楼主给出抽样 样本 来实际反驳 -------------------------------------------------------------------- 另外从执行角度而言,try等于是在method(也可能是其他的如property)层面又加了一层statement, 也就是多了一层副本,如果出错可以保证回滚的机制,本身会有一定的性能开销 ,如果try中间的语句较多,相信也会很大程度上影响测试结果
clark523 2013-10-27
  • 打赏
  • 举报
回复
引用 25 楼 sj178220709 的回复:
cDic = dic[key]; 这个地方,楼主改一下,再测试看看。 try会额外消耗性能,这是肯定的, 但是你第一种多查询了一次。 至于第二种方法,还要看cache命中率,如果很高,第二种还有点优势,但如果命中率比较低,则异常引发次数太多,肯定不行。 不过归根结底来说,TryGetValue还是最好的,代码更简洁优美,这种内存操作,性能从来不是问题,优化的意义不大。
这里为什么我说第二种是优化后的代码呢。 因为try catch的性能损耗是低于 Check的性能损耗的。 而无论命中率如何,都会进行一次Check. 这就是优化点了。 另外。 并不是说这个优化的意义有多大。 只是我觉得这个是一个非常新的优化思路。也许在我这个场景只是很小的优化。 但是当你觉得无法再进行优化时,这个思路可能会开阔你的视野。 这也是我为什么要分享在论坛里面的初衷。 或许我这个解决方案比较片面。 但是到目前为止还没有看见任何一个同学在事实上来推翻我这个优化是错误的证据。 只看见很多都是觉得不行,觉得不对。觉得没有意义。 这让我对于本论坛很失望。
clark523 2013-10-27
  • 打赏
  • 举报
回复
引用 25 楼 sj178220709 的回复:
cDic = dic[key]; 这个地方,楼主改一下,再测试看看。 try会额外消耗性能,这是肯定的, 但是你第一种多查询了一次。 至于第二种方法,还要看cache命中率,如果很高,第二种还有点优势,但如果命中率比较低,则异常引发次数太多,肯定不行。 不过归根结底来说,TryGetValue还是最好的,代码更简洁优美,这种内存操作,性能从来不是问题,优化的意义不大。
唉。。。还是没有办法脱离传统思想。 TryGetValue和我的第一种效率是完全一样的。
clark523 2013-10-26
  • 打赏
  • 举报
回复
引用 21 楼 caozhy 的回复:
我发现吃自助餐前先拉泡屎再去吃,我就可以多赚一点,这么创新的成果,有没有必要向大家汇报下。
你这水平是怎么当上的版主啊?
clark523 2013-10-26
  • 打赏
  • 举报
回复
引用 18 楼 sp1234 的回复:
如果是100W次加起来发现有100~200ms差别,那么这基本上就可以认定为没有什么差别。
请问你知道100w执行所需要的时间么? 是优化前是1.4秒,优化后是1.2秒也就是说是10%-20%的提速。能叫没什么差别??? 另。请看我7楼的回复。里面已经解释清楚了。为什么不用TryGetValue.
缪军 2013-10-25
  • 打赏
  • 举报
回复
引用 21 楼 caozhy 的回复:
我发现吃自助餐前先拉泡屎再去吃,我就可以多赚一点,这么创新的成果,有没有必要向大家汇报下。
这么龌龊的回复你也说得出口, 你还记得当初怎么牛逼烘烘的毛遂自荐来当版主的么? 你骂人骂习惯了,累教不改
threenewbee 2013-10-25
  • 打赏
  • 举报
回复
我发现吃自助餐前先拉泡屎再去吃,我就可以多赚一点,这么创新的成果,有没有必要向大家汇报下。
  • 打赏
  • 举报
回复
哦,看错了。sorry。 我同意#17楼的意见。基本上我们没有先查询然后再查询,都是使用 TryGetValue 一次查询的。所以看到你的代码实在是太陌生了,所以一下子没有看懂。
  • 打赏
  • 举报
回复
你的代码有一点费解: Dictionary<Type, Dictionary<string, RegistObjectContext>()> dic; if(!dic.ContainsKey(key)){ dic.Add(key, new Dictionary<string, RegistObjectContext>()); } cDic = dic[key]; 为什么插入之后,再查找一遍?直接写 Dictionary<Type, Dictionary<string, RegistObjectContext>()> dic; cDic = new Dictionary<string, RegistObjectContext>() if(!dic.ContainsKey(key)) dic.Add(key, cDic); 岂不是更好嘛!
  • 打赏
  • 举报
回复
如果是100W次加起来发现有100~200ms差别,那么这基本上就可以认定为没有什么差别。
showjim 2013-10-25
  • 打赏
  • 举报
回复
最好是用TryGetValue
cheng2005 2013-10-25
  • 打赏
  • 举报
回复
对不起,我踩到您的尾巴了。
clark523 2013-10-25
  • 打赏
  • 举报
回复
算了。懒得搭理你了。你就不是来讨论问题的。而是不知道在哪里炫耀什么莫名的优越感的。。
clark523 2013-10-25
  • 打赏
  • 举报
回复
引用 12 楼 wddw1986 的回复:
[quote=引用 11 楼 clark523 的回复:] 请拿事实说话,即使是你说的情况,也不一定是第一种效率高。 请注意,我每次都少了一次FindEntry的执行。 你通过什么判断的FindEntry的执行消耗是低于Try Catch???
麻烦你把我的回帖 [size=48px]好好[/size] 看一遍,我哪句话说过第一种效率高?[/quote]
引用 9 楼 wddw1986 的回复:
测试样本的构成是会对结果造成影响的。最简单的例子,如果你的值大部分已经在缓存里了,那么必然是第二种效率高,如果你的缓存是空的,每次取得都是新值,那么必然是前一种高。 你分析过测试样本吗?有吗?
这句话不是你说的?
clark523 2013-10-25
  • 打赏
  • 举报
回复
引用 10 楼 wddw1986 的回复:
另外 cDic = dic[pType]; 这个也是有问题的。 新插入的没必要再去取一遍,为了省一行代码结果多一次查找。
你说得没错。 这一次查找是有点没有意义,如果是新插入的话。 但是貌似这个是不是我这篇帖子的重点吧? 我的帖子是想通过这个例子来说明我们在写代码的时候可以用一些更巧妙的思路来优化性能。
cheng2005 2013-10-25
  • 打赏
  • 举报
回复
引用 11 楼 clark523 的回复:
请拿事实说话,即使是你说的情况,也不一定是第一种效率高。 请注意,我每次都少了一次FindEntry的执行。 你通过什么判断的FindEntry的执行消耗是低于Try Catch???
麻烦你把我的回帖 [size=48px]好好[/size] 看一遍,我哪句话说过第一种效率高?
加载更多回复(11)
日历控件类名:CalendarMonthControl 开发平台:Visual Studio 2008 字符集:使用 Unicode 字符集 此项目的源码涉及到了以下方面的知识和技术: 1. Windows窗体坐标(逻辑坐标,客户区坐标概念)。 2. 图像自绘(不是MFC来绘制而是自已来管理一切)。 3. 双缓冲技术运用(有时侯自绘的控件可能会有“闪烁感”,在这里你可以找到解决方法)。 4. STL运用(软件开发第一准则:“不要重复轮子”)。 5. DirectUI思想(思想决定一切)。 6. C++设计模式(清晰的构架对一个项目来说是至关重要的,而无论这个项目大小) 项目介绍: 虽然就技术上来讲重新设计和开发一个类似于Windows时间控件的技术并不难,但是如果能够在能够很好很方便的运行的基础上再考虑到美观,易用,可扩展性,构架清晰等方面那也不算太容易。从某种意义上来说这个项目算得上是STL的一个小作品,因为此项目所使用的数据结构和算法全部是由STL所提供的,由于大量使用STL来进行数据处理,以及在图像绘制方面使用了流行的双缓冲技术所以整个系统能够得到极高的性能,当然即便如此此项目仍然还有许多可以优化的空间,它的性能还没有被发挥到最优,例如显示月份的单元格是由vector动态的push_back,但是如果你认真分析后会发现实际上这些单元格是固定的,也就是说在构函数就可以new一个固定大小的内存空间来供vector使用,而不用每次都动态的push_back。我在整个项目的设计上花了一些时间来保证代码的层次结构清晰,同时也使用了一些C++里面的高级技术,例如多重继承,重载,虚函数,虽然不多,但是尽量保证了恰到好处。 作为一个程序开发的新手,或许能够从这个项目得到一些思路。实际上这个项目被重写过一次,第一次的设计完全是面向过程的,谈不上设计,因为整个项目就是一个类,或许这段代码在以前可能称得上算是一个“牛X”的作品,但是在设计模式大行其道的今天最多算得上是污染开发人员视觉的产物,优其可读性和扩展性均不能达到要求,这一版本是完全基于面向对象的思想来进行设计的,所以比上一个版本更容易阅读也更具层次感觉。 此项目源代码虽然不多,但是在许多地方使用到的一些技巧仍然值得新手学习,例如在与用户交互事件设计及处理方面(OnClick,OnMove)就使用了DirectUI的思想,控件将月份和年份的区域当作一个逻辑区域来对待,将每一天或每一月看作为一块小的逻辑区域(用RECT标识),最终将这些区域的逻辑图形绘制到一个DC上,同时使用统一的数据管理对象来管理这些区域的事件及处理(极到位的运用了C++类设计原则的部分思想),这样一方面保证了绘制效率,另一方面增强了控件的可扩展性和可管理。这也正是DirectUI的核心思想。 最后非常感谢你花时间来阅读这些文字,同时希望你认为花费这些时间是值得的。

110,545

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧