我们都知道,过早优化是所有邪恶的根源,因为它会导致不可读/不可维护的代码.更糟糕的是悲观化,当有人实施"优化",因为他们认为它会更快,但它最终会变慢,而且变得越来越慢,不可维护等等.你看到的最荒谬的例子是什么? ?
我认为"过早优化是所有邪恶的根源"这句话是方式,过度使用.对于许多项目而言,它已成为在项目后期才考虑绩效的借口.
这句话通常是人们避免工作的拐点.当人们真正说出"哎呀,我们真的没有想到这一点并且现在没有时间处理它"时,我看到了这句话.
我看到了更多"荒谬"的愚蠢性能问题的例子,而不是由于"悲观化"引入的问题的例子
在程序启动期间读取相同的注册表项数千(或十万)次.
加载相同的DLL数百或数千次
通过不必要地保留文件的完整路径来浪费大量字节的内存
不组织数据结构,因此它们占用的内存超出了他们的需要
调整存储文件名或路径到MAX_PATH的所有字符串
对有事件,回调或其他通知机制的事件进行无偿轮询
我认为更好的说法是:"没有测量和理解的优化根本不是优化 - 它只是随机变化".
良好的性能工作非常耗时 - 通常更多的是功能或组件本身的开发.
数据库是悲观化的游戏.
收藏夹包括:
将表拆分为多个(按日期范围,字母范围等),因为它"太大".
为退役记录创建归档表,但继续使用生产表UNION它.
通过(部门/客户/产品/等)复制整个数据库
抵制向索引添加列,因为它太大了.
创建大量汇总表,因为从原始数据重新计算太慢.
使用子字段创建列以节省空间.
非规范化为字段作为数组.
这是我的头脑.
我认为没有绝对的规则:有些东西最好在前期优化,有些则不是.
例如,我在一家公司工作,我们收到了来自卫星的数据包.每个数据包都需要花费很多钱,因此所有数据都经过了高度优化(即打包).例如,纬度/经度不是作为绝对值(浮动)发送的,而是作为相对于"当前"区域的"西北"角的偏移.在使用之前我们必须解压缩所有数据.但我认为这不是悲观,而是智能优化以降低通信成本.
另一方面,我们的软件架构师决定将解压缩的数据格式化为一个非常易读的XML文档,并将其存储在我们的数据库中(而不是将每个字段存储在相应的列中).他们的想法是"XML是未来","磁盘空间便宜","处理器便宜",所以没有必要优化任何东西.结果是我们的16字节数据包变成了存储在一列中的2kB文档,即使是简单的查询,我们也必须在内存中加载兆字节的XML文档!我们每秒收到超过50个数据包,所以你可以想象这个表现有多糟糕(BTW,公司破产).
再说一次,没有绝对的规则.是的,有时过早优化是一个错误.但有时候"cpu /磁盘空间/内存便宜"的座右铭是所有邪恶的真正根源.
在一个旧项目中,我们继承了一些(非常优秀的)具有大量Z-8000经验的嵌入式系统程序员.
我们的新环境是32位Sparc Solaris.
其中一个人把所有的注册改为短路以加速我们的代码,因为从RAM中获取16位比抓取32位更快.
我不得不编写一个演示程序,以显示在32位系统上获取32位值比抓取16位值更快,并解释为了获取16位值,CPU必须使32位宽存储器访问然后屏蔽掉或移位16位值不需要的位.
哦,主啊,我想我已经看过他们了.通常情况下,这是一个努力解决性能问题的人,他们过于懒惰,无法解决这些性能问题的原因,甚至研究是否确实存在性能问题.在许多这种情况下,我想知道这不仅仅是那个想要尝试特定技术并且拼命寻找适合他们闪亮的新锤子的钉子的情况.
这是最近的一个例子:
数据架构师向我提出了一个精心设计的建议,即在一个相当大而复杂的应用程序中垂直分区密钥表.他想知道为适应变化需要哪种类型的开发工作.谈话是这样的:
我:你为什么这么想?你想解决的问题是什么?
他:表X太宽,我们因性能原因对其进行分区.
我:是什么让你觉得它太宽了?
他:顾问说,在一张桌子上有太多的栏目.
我:这会影响性能吗?
他:是的,用户报告了应用程序XYZ模块的间歇性减速.
我:你怎么知道桌子的宽度是问题的根源?
他:这是XYZ模块使用的关键表,它就像200列.一定是问题所在.
我(解释):但模块XYZ特别使用该表中的大多数列,并且它使用的列是不可预测的,因为用户配置应用程序以显示他们想要从该表显示的数据.很可能95%的时间我们最终将所有桌子连接在一起,这会损害性能.
他:顾问说它太宽了我们需要改变它.
我:这位顾问是谁?我不知道我们聘请了一位顾问,他们也没有和开发团队谈过话.
他:嗯,我们还没有雇用他们.这是他们提供的提案的一部分,但他们坚持认为我们需要重新构建这个数据库.
我:嗯嗯.因此,销售数据库重新设计服务的顾问认为我们需要重新设计数据库....
谈话继续这样下去.之后,我又看了一下有问题的表,并确定它可能会通过一些简单的规范化来缩小,而不需要异乎寻常的分区策略.当我调查性能问题(之前未报告过)并将其跟踪到两个因素时,这当然是一个有争议的问题:
缺少几个关键列的索引.
一些流氓数据分析师通过直接使用MSAccess查询生产数据库来定期锁定密钥表(包括"太宽"的表).
当然,架构师仍然在推动桌子的垂直分区,这个分区挂在"太宽"的元问题上.他甚至通过获得另一位数据库顾问的提议来支持他的案例,该顾问能够确定我们需要对数据库进行重大设计更改,而无需查看应用程序或运行任何性能分析.
我见过人们使用alphadrive-7来完全孵化CHX-LT.这是一种不常见的做法.更常见的做法是初始化ZT变换器,以减少缓冲(由于更大的净过载阻力)并创建java样式字节图.
完全悲观!
我承认,没有什么是惊天动地的,但是我发现人们使用StringBuffer来连接Java中循环之外的字符串.这很像转弯一样简单
String msg = "Count = " + count + " of " + total + ".";
成
StringBuffer sb = new StringBuffer("Count = "); sb.append(count); sb.append(" of "); sb.append(total); sb.append("."); String msg = sb.toString();
过去常常在循环中使用该技术,因为它的速度要快得多.问题是,StringBuffer是同步的,所以如果你只连接几个字符串,实际上会有额外的开销.(更不用说这种差异在这个规模上绝对是微不足道了.)关于这种做法的另外两点:
StringBuilder是不同步的,因此在无法从多个线程调用代码的情况下,应优先使用StringBuffer.
无论如何,现代Java编译器会将可读的字符串串联转换为优化的字节码.
我曾经看过一个使用'Root'表的MSSQL数据库.Root表有四列:GUID(uniqueidentifier),ID(int),LastModDate(datetime)和CreateDate(datetime).数据库中的所有表都是Root表的外键.每当在db 中的任何表中创建新行时,您必须使用几个存储过程在Root表中插入一个条目,然后才能到达您关心的实际表(而不是数据库执行的工作你有几个触发器简单的触发器).
这造成了一堆无用的无意中听到和头痛,需要在它上面写任何东西来使用sprocs(并且消除了我将LINQ引入公司的希望.这是可能的,但不值得头疼),并且最重要的是没有'甚至完成它应该做的事情.
选择此路径的开发人员在假设这节省了大量空间的情况下为其辩护,因为我们没有在表本身上使用Guids(但是...不是我们制作的每一行在Root表中生成的GUID?) ,以某种方式改进了性能,并使审计对数据库的更改变得"容易".
哦,数据库图看起来像是来自地狱的突变蜘蛛.
POBI怎么样 - 意图明显的悲观化?
90年代我的同事厌倦了被首席执行官罢工,因为首席执行官在每个ERP软件(定制版)发布的第一天花费了新功能中的定位性能问题.即使新的功能已经达到千兆字节并使不可能成为可能,他总是会发现一些细节,甚至是看似重大的问题.他相信对编程有很多了解,并通过踢程序员驴来解决问题.
由于批评的无能(他是首席执行官,而不是IT人),我的同事从来没有设法做到这一点.如果你没有性能问题,你就无法消除它......
直到一个版本,他把很多Delay(200)函数调用(它是Delphi)放入新代码中.上线后仅用了20分钟,他就被命令出现在首席执行官办公室,以便亲自接受他过期的侮辱.
到目前为止,只有不寻常的事情是我的同事们回来时微笑,开玩笑,出去买一两个BigMac,而他通常会踢桌子,对首席执行官和公司大加抨击,剩下的时间都会变成死亡.
当然,我的同事现在在他的办公桌休息了一两天,提高了他在Quake中的瞄准技能 - 然后在第二天或第三天他删除了延迟电话,重建并发布了一个"紧急补丁",他传播了这个词他花了2天1夜来修复表演漏洞.
这是邪恶的首席执行官说"干得好"的第一个(也是唯一的)时间.给他.这一切都很重要,对吗?
这是真正的POBI.
但它也是一种社交流程优化,所以100%可以.
我认为.
"数据库独立性".这意味着没有存储过程,触发器等 - 甚至没有任何外键.
var stringBuilder = new StringBuilder(); stringBuilder.Append(myObj.a + myObj.b + myObj.c + myObj.d); string cat = stringBuilder.ToString();
最好的使用我见过的StringBuilder.
当一个简单的string.split足够时,使用正则表达式来分割字符串
我知道这个帖子已经很晚了,但我最近看到了这个:
bool isFinished = GetIsFinished(); switch (isFinished) { case true: DoFinish(); break; case false: DoNextStep(); break; default: DoNextStep(); }
你知道吗,以防布尔值有一些额外的值......
最糟糕的例子我能想到的是我公司的一个内部数据库,其中包含所有员工的信息.它从HR获得每晚更新,并在顶部提供ASP.NET Web服务.许多其他应用程序使用Web服务来填充搜索/下拉字段等内容.
悲观的是,开发人员认为重复调用Web服务的速度太慢,无法重复进行SQL查询.他做了什么?应用程序启动事件读入整个数据库并将其全部转换为内存中的对象,无限期地存储,直到应用程序池被回收.这段代码非常慢,在不到2000名员工中加载需要15分钟.如果您在白天无意中回收了应用程序池,则可能需要30分钟或更长时间,因为每个Web服务请求都会启动多个并发重新加载.出于这个原因,新员工在他们创建帐户的第一天就不会出现在数据库中,因此在他们的前几天无法访问大多数内部应用程序,他们会大笑.
第二个悲观主义是开发经理不想触及它,因为害怕破坏依赖的应用程序,但是由于这样一个简单组件的设计很差,我们仍然在公司范围内零星地停止关键应用程序.
似乎没有人提到排序,所以我会.
几个不同的时间,我发现有人手工制作了一个bubort,因为情况"不需要"调用已经存在的"过于花哨"的快速排序算法.当他们的手工制作的Bubbleort在他们用于测试的十行数据上运行良好时,开发人员感到满意.在客户添加了几千行之后,它并没有完全消失.
我曾经在一个充满这样代码的应用程序上工作过:
1 tuple *FindTuple( DataSet *set, int target ) { 2 tuple *found = null; 3 tuple *curr = GetFirstTupleOfSet(set); 4 while (curr) { 5 if (curr->id == target) 6 found = curr; 7 curr = GetNextTuple(curr); 8 } 9 return found; 10 }
只需删除found
,最后返回null
,并将第六行更改为:
return curr;
应用程序性能翻倍.
我曾经不得不尝试在Constants类中修改包含这些gem的代码
public static String COMMA_DELIMINATOR=","; public static String COMMA_SPACE_DELIMINATOR=", "; public static String COLIN_DELIMINATOR=":";
为了不同的目的,这些中的每一个在应用的其余部分中被多次使用.COMMA_DELIMINATOR在8个不同的软件包中占用了200多个代码.
我在内部软件中一次又一次地遇到的最重要的一次:
出于"可移植性"的原因不使用DBMS的功能,因为"我们可能希望以后切换到其他供应商".
读我的唇语.对于任何内部工作:它不会发生!
我有一个同事试图战胜我们的C编译器的优化器和常规重写代码,只有他才能阅读.他最喜欢的一个技巧是改变一种可读的方法,比如(编写一些代码):
int some_method(int input1, int input2) { int x; if (input1 == -1) { return 0; } if (input1 == input2) { return input1; } ... a long expression here ... return x; }
进入这个:
int some_method() { return (input == -1) ? 0 : (input1 == input2) ? input 1 : ... a long expression ... ... a long expression ... ... a long expression ... }
也就是说,一次可读方法的第一行将变为" return
",所有其他逻辑将被深层嵌套的三元表达式替换.当你试图争论这是如何不可维护时,他会指出他的方法的汇编输出是三或四个汇编指令更短的事实.这不一定任何更快,但它始终是一个小小的有点短.这是一个嵌入式系统,其中内存使用偶尔会起作用,但是可以做出比这更容易的优化,这将使代码可读.
然后,在此之后,由于某种原因,他认为这ptr->structElement
太难以理解,所以他开始将所有这些改变为(*ptr).structElement
理论上它更具可读性和更快性.
将可读代码转换为不可读代码,最多可提高1%,有时实际上代码更慢.
在我作为一名成熟的开发人员的第一份工作中,我接手了一个项目,该项目正在遭遇扩展问题.它在小数据集上运行得相当好,但在给定大量数据时会完全崩溃.
当我进入时,我发现原始程序员试图通过并行化分析来加快速度 - 为每个额外的数据源启动一个新线程.然而,他犯了一个错误,因为所有线程都需要一个共享资源,他们在这个资源上陷入僵局.当然,并发的所有好处都消失了.此外,它破坏了大多数系统以启动100多个线程,只有其中一个线程锁定.我的强劲开发机器是一个例外,因为它在大约6小时内通过150源数据集进行搅拌.
因此,为了解决这个问题,我删除了多线程组件并清理了I/O. 在没有其他更改的情况下,150源数据集的执行时间在我的机器上降至10分钟以下,从普通公司机器的无限小时降至半小时以下.
我想我可以提供这个宝石:
unsigned long isqrt(unsigned long value) { unsigned long tmp = 1, root = 0; #define ISQRT_INNER(shift) \ { \ if (value >= (tmp = ((root << 1) + (1 << (shift))) << (shift))) \ { \ root += 1 << shift; \ value -= tmp; \ } \ } // Find out how many bytes our value uses // so we don't do any uneeded work. if (value & 0xffff0000) { if ((value & 0xff000000) == 0) tmp = 3; else tmp = 4; } else if (value & 0x0000ff00) tmp = 2; switch (tmp) { case 4: ISQRT_INNER(15); ISQRT_INNER(14); ISQRT_INNER(13); ISQRT_INNER(12); case 3: ISQRT_INNER(11); ISQRT_INNER(10); ISQRT_INNER( 9); ISQRT_INNER( 8); case 2: ISQRT_INNER( 7); ISQRT_INNER( 6); ISQRT_INNER( 5); ISQRT_INNER( 4); case 1: ISQRT_INNER( 3); ISQRT_INNER( 2); ISQRT_INNER( 1); ISQRT_INNER( 0); } #undef ISQRT_INNER return root; }
由于平方根在一个非常敏感的地方计算,我的任务是寻找一种方法来使它更快.这种小型重构将执行时间减少了三分之一(对于所使用的硬件和编译器的组合,YMMV):
unsigned long isqrt(unsigned long value) { unsigned long tmp = 1, root = 0; #define ISQRT_INNER(shift) \ { \ if (value >= (tmp = ((root << 1) + (1 << (shift))) << (shift))) \ { \ root += 1 << shift; \ value -= tmp; \ } \ } ISQRT_INNER (15); ISQRT_INNER (14); ISQRT_INNER (13); ISQRT_INNER (12); ISQRT_INNER (11); ISQRT_INNER (10); ISQRT_INNER ( 9); ISQRT_INNER ( 8); ISQRT_INNER ( 7); ISQRT_INNER ( 6); ISQRT_INNER ( 5); ISQRT_INNER ( 4); ISQRT_INNER ( 3); ISQRT_INNER ( 2); ISQRT_INNER ( 1); ISQRT_INNER ( 0); #undef ISQRT_INNER return root; }
当然,有更快更好的方法来做到这一点,但我认为这是一个非常巧妙的悲观化例子.
编辑:想想看,展开的循环实际上也是一个整洁的悲观.通过版本控制挖掘,我也可以展示重构的第二阶段,其表现甚至比上面更好:
unsigned long isqrt(unsigned long value) { unsigned long tmp = 1 << 30, root = 0; while (tmp != 0) { if (value >= root + tmp) { value -= root + tmp; root += tmp << 1; } root >>= 1; tmp >>= 2; } return root; }
这是完全相同的算法,虽然实现略有不同,所以我认为它符合条件.
这可能是你所追求的更高水平,但修复它(如果你被允许)也会带来更高的痛苦程度:
坚持手动滚动对象关系管理器/数据访问层,而不是使用已建立的,经过测试的成熟库之一(即使在他们被指出之后).
所有外键约束都从数据库中删除,否则会出现很多错误.
在每次javascript操作之前检查是否存在您正在操作的对象.
if (myObj) { //or its evil cousin, if (myObj != null) { label.text = myObj.value; // we know label exists because it has already been // checked in a big if block somewhere at the top }
我对这类代码的问题是,如果它不存在,似乎没有人关心它是什么?什么都不做?不要向用户提供反馈?
我同意Object expected
错误是令人讨厌的,但这不是最好的解决方案.
这不完全适合这个问题,但无论如何我都会提到一个警示故事.我正在研究一个运行缓慢的分布式应用程序,然后飞到DC参加主要旨在解决问题的会议.项目负责人开始概述旨在解决延迟的重新架构.我自告奋勇说我周末采取了一些测量方法,将瓶颈分离为单一方法.事实证明,本地查找中缺少记录,导致应用程序必须在每次事务处都转到远程服务器.通过将记录添加回本地商店,延迟被消除 - 问题得到解决.请注意,重新架构不会解决问题.
YAGNI极端主义怎么样?这是一种过早悲观化的形式.似乎你在任何时候申请YAGNI,然后你最终需要它,导致添加它的努力是你在开始时添加它的10倍.如果你创建了一个成功的程序,那么你可能需要它.如果你习惯于创建生命快速耗尽的程序,那么继续练习YAGNI因为那时我想YAGNI.
不完全是过早的优化 - 但肯定是错误的 - 这是在BBC网站上从一篇讨论Windows 7的文章中读到的.
Curran先生表示,Microsoft Windows团队一直在研究操作系统的各个方面以进行改进."通过略微修剪WAV文件关闭音乐,我们能够在关机时间内缩短400毫秒.
现在,我还没有尝试过Windows 7,所以我可能错了,但是我愿意打赌,其中有一些问题比关闭需要多长时间更重要.毕竟,一旦我看到"关闭Windows"消息,显示器就会关闭,我正在走开 - 400毫秒的时间对我有什么好处?
我部门的某个人曾经写过一个字符串类.一个接口CString
,但没有Windows依赖.
他们所做的一个"优化"是不分配超过必要的内存.显然没有意识到类这样的原因std::string
会分配多余的内存,因此一系列+=
操作可以在O(n)时间内运行.
相反,每一次+=
调用都强制重新分配,而这又重复出现在一个O(n²)施莱米尔画家的算法中.
我的一位前同事(实际上是一名员工)被指派为我们的Java ERP构建一个新模块,该模块应该收集并分析客户的数据(零售行业).他决定在其组件中分割每个日历/日期时间字段(秒,分钟,小时,日,月,年,星期几,bimester,三个月(!)),因为"我还会如何查询'每个星期一'?"