Skip to content

Commit 37ce72a

Browse files
committed
add_chinese_file
1 parent 22cf5f2 commit 37ce72a

File tree

73 files changed

+1231
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+1231
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# 学会Debug
2+
3+
调试(Debug)是作为一个程序员的基石。调试这个词第一个含义即是移除错误,但真正有意义的含义是,通过检查来观察程序的运行。一个不能调试的程序员等同于瞎子。
4+
5+
那些认为设计、分析、复杂的理论或其他东西是更基本的东西的理想主义者们不是现实的程序员。现实的程序员不会活在理想的世界里。即使你是完美的,你周围也会有,并且也需要与主要的软件公司或组织,比如GNU,或者与你的同事,写的代码打交道。这里面大部分的代码以及它们的文档是不完美的。如果没有获得代码的执行过程可见性的能力,最轻微的颠簸都会把你永远地抛出去。通常这种可见性只能从实验获得,也就是,调试。
6+
7+
调试是一件与程序运行相关的事情,而非与程序本身相关。你从主要的软件公司购买一些产品,你通常不会看到(产品背后的)程序本身。但代码不遵循文档这样的情况(让你整台机器崩掉是一个常见又特殊的例子)或者文档没有说明的情况仍然会出现,不可避免的,这意味着你做的一些假设并不对,或者一些你没有预料到的情况发生了。有时候,神奇的修改源代码的技巧可能会生效。当它无效时,你必须调试了。
8+
9+
为了获得一个程序执行的可见性,你必须能够执行代码并且从这个过程中观察到什么。有时候这是可见的,比如一些正在呈现在屏幕上的东西,或者两个事件之间的延迟。在许多其他的案例中,它与一些不一定可见的东西相关,比如代码中一些变量的状态,当前真正在执行的代码行,或者是否一些断言持有了一个复杂的数据结构。这些隐藏的细节必须被显露出来。
10+
11+
12+
通常的(一些)观察一个正在执行的程序的内部的方法可以如下分类:
13+
14+
- 使用一个调试工具;
15+
- Printlining[(戳这里看释义)](../../4-Glossary.md) - 对程序做一个临时的修改,通常是加一些行去打印一些信息;
16+
- 日志 - 用日志的形式为在程序的运行中创建一个永久的视窗。
17+
18+
当调试工具稳定可用时,它们是非常美妙的,但[Printlining](../../4-Glossary.md)和写日志是更加重要的。调试工具通常落后于编程语言的发展,所以在任何时间点它们都可能是无效的。另外,调试工具可能轻微改变程序实际执行的方式。最后,调试有许多种,比如检查一个断言和一个巨大的数据结构,这需要写代码并改变程序的运行。当调试工具可用时,知道怎样使用调试工具是好的,但学会使用其他两种方式是至关重要的。
19+
20+
当需要修改代码时,一些初学者会害怕调试。这是可以理解的,这有点像探索型外科手术。但你需要学会打破代码,让它跳起来,你需要学会在它上面做实验,并且需要知道你临时对它做的任何事情都不会使它变得更糟。如果你感受到了这份恐惧,找一位导师 - (否则)在许多人面对这种恐惧的脆弱的开始时刻,我们会因此失去很多优秀的程序员。
21+
22+
Next [如何通过分离问题空间来Debug](02-How to Debug by Splitting the Problem Space.md)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# 如何通过分割问题空间来Debug
2+
3+
调试是有趣的,因为它一开始是个迷。你认为它应该这样做,但实际上它却那样做。很多时候并不仅是这么简单---我给出的任何例子都会被设计来与一些偶尔在现实中会发生的情况相比较。调试需要创造力与智谋。如果说调试有简单之道,那就是在这个谜题上使用分治法。
4+
5+
假如,你创建了一个程序,它会在一个序列里做十件事情。当你运行它的时候,它崩溃了。因为你写的代码并不想让它崩溃,所以现在你有一个谜题了。当你查看输出时,你可以看到序列里前七件事情运行成功了。最后三件事情在输出里却看不到,所以你的谜题变小了:“它是在执行第8、9、10件事的时候崩溃的”。
6+
7+
你可以设计一个实验来观察它是在哪件事情上崩溃的吗?当然,你可以用一个调试器或者我们可以在第8第9件事后面加一些[printlining](../../4-Glossary.md)的语句(或者你正在使用的任何语言里的等价的事情),当我们重新运行它的时候,我们的谜题会变得更小,比如“它是在做第九件事的时候崩溃的”。我发现,把谜题是怎样的一直清楚地记在心里能让我们保持注意力。当几个人在一个问题的压力下一起工作时,很容易忘记最重要的谜题是什么。
8+
9+
调试技术中分治的关键和算法设计里的分治是一样的。你只要从中间开始划分,就不用划分太多次,并且能快速地调试。但问题的中点在哪里?这就是真正创造力和经验需要参与的地方。
10+
11+
对于一个真正的初学者来说,可能发生错误的地方好像在代码的每一行里都有。一开始,你看不到一些其他的你稍后将会学到的维度,比如执行过的代码段,数据结构,内存管理,与外部代码的交互,一些有风险的代码,一些简单的代码。对于一个有经验的程序员,这些其他的维度为整个可能出错的事情展示了一个不完美但是有用的思维模型。拥有这样的思维模型能让一个人更高效地找到谜题的中点。
12+
13+
一旦你最终划分出了所有可能出错的地方,你必须试着判断错误躲在哪个地方。比如:这样一个谜题,哪一行未知的代码让我的程序崩溃了?你可以这样问自己,出错的代码是在我刚才执行的程序中间的那行代码的前面还是后面?通常你不会那么幸运就能知道错误在哪行代码甚至是哪个代码块。通常谜题更像这个样子的:“图中的一个指针指向了错误的结点还是我的算法里变量自增的代码没有生效?”,在这种情况下你需要写一个小程序去确认图中的指针是否都是对的,来决定分治后的哪个部分可以被排除。
14+
15+
Next [如何移除错误](03-How to Remove an Error.md)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# 如果移除一个错误
2+
3+
我曾有意把检查程序执行和修复错误分割开来,但是当然,调试也意味着移除bug。理想状况下,当你完美的发现了错误以及它的修复方法时,你会对代码有完美的理解,并且有一种顿悟(啊哈!)的感觉。但由于你的程序会经常使用不具有可视性的、没有一致性注释的系统,所以完美是不可能的。在其他情况下,可能代码是如此的复杂以至于你的理解可能并不完美。
4+
5+
在修复bug时,你可能想要做最小的改变来修复它。你可能看到一些其他需要改进的东西,但不会同时去改进他们。试图使用科学的方法去改进一个东西,并且一次只改变一个东西。修复bug最好的方式是能够重现bug,然后把你的修复替换进去,重新运行你的程序,观察bug不再出现。当然,有时候不止一行代码需要修改,但你在逻辑上仍然需要使用一个独立原子(译者注:以前人们认为原子不可再分,所以用用原子来代表不可再分的东西)的改变来修复这个bug。
6+
7+
有时候,可能实际上有几个bug,但表现出来好像是一个。这取决于你怎么定义bug,你需要一个一个地修复它们。有时候,程序应该做什么或者原始作者想要做什么是不清晰的。在这种情况下,你必须多加练习,增加经验,评判并为代码赋予你自己的认知。决定它应该做什么,并注释/或用其他方式阐述清楚,然后修改代码以遵循你赋予的含义。这是一个进阶或高级的技能,有时甚至比一开始用原始的方式创建这些代码还难,但真实的世界经常是混乱的。你必须修复一个你不能重写的系统。
8+
9+
Next [如何使用日志调试](04-How to Debug Using a Log.md)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# 如何使用日志调试
2+
3+
*Logging*(日志)是一种编写系统的方式,可以产生一系列信息记录,被称为log。*Printlining*只是输出简单的,通常是临时的日志。初学者一定要理解并且使用日志,因为他们对编程的理解是局限的。因为系统的复杂性,系统架构必须理解与使用日志。理想地,程序运行时,日志产生的信息的数量需要是可配置的。通常,日志提供了下面三个基本的优点:
4+
5+
- 日志可以提供一些难以重现的bug的有效信息,比如在产品环境中发生的、不能在测试环境重现的bug。
6+
- 日志可以提供统计和与性能相关的数据,比如语句间流逝过的时间。
7+
- 可配置的情况下,日志允许我们获取普通的信息,使得我们可以在不修改或重新部署代码的情况下调试以处理具体的问题。
8+
9+
需要输出的日志数量总是一个简约与信息的权衡。太多的信息会使得日志变得昂贵,并且造成[*滚动目盲*](../../4-Glossary.md),使得发现你想要的信息变得很困难。但信息太少的话,日志可能不包含你需要的信息。出于这个原因,让日志的输出可配置是非常有用的。通常,日志中的每个记录会标记它在源代码里的位置,执行它的线程(如果可用的话),时间精度,并且,通常有,一些额外的有效信息,比如一些变量的值,剩余内存大小,数据对象的数量,等等。这些日志语句撒遍源码,但只出现在主要的功能点和一些可能出现危机的代码里。每个语句可以被赋予一个等级,并且将会在系统设置输出这个等级时输出这个记录。你应该设计好日志语句来标记你预期的问题。预估测量程序表现的必要性。
10+
11+
如果你有一个永久的日志,printling现在可以用日志的形式来完成,并且一些调试语句可能会永久地加入日志系统。
12+
13+
Next [如何理解性能问题](05-How to Understand Performance Problems.md)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# 如何理解性能问题
2+
3+
学习理解运行的程序的性能问题与学习debug是一样不可避免的。即使你完美地理解了你写的代码的代价,你的代码也会调用其他你几乎不能控制的或者几乎不可看透的软件系统。然而,实际上,通常性能问题和调试有点不一样,而且往往要更简单些。
4+
5+
假如你或你的客户认为你的一个系统或子系统运行太慢了。在你把它变快之前,你必须构建一个它为什么慢的思维模型。为了做到这个,你可以使用一个图表工具或者一个好的日志,去发现时间或资源真正被花费在什么地方。有一句很有名的格言:90%的时间会花费在10%的代码上。在性能这个话题上,我想补充的是输入输出开销的重要性。通常大部分时间是以某种形式花费在I/O上。发现昂贵的I/O和昂贵的10%代码是构建思维模型的一个好的开始。
6+
7+
计算机系统的性能有很多个维度,很多资源会被消耗。第一种资源是“挂钟时间”,即执行程序的所有时间。记录“挂钟时间”是一件特别有价值的事情,因为它可以告诉我们一些图表工具表现不了的不可预知的情况。然而,这并不总是描绘了整幅图景。有时候有些东西只是花费了稍微多一点点时间,并且不会引爆什么问题,所以在你真实要处理的计算机环境中,多一些处理器时间可能会是更好的选择。相似的,内存,网络带宽,数据库或其他服务器访问,可能最后都比处理器时间要更加昂贵。
8+
9+
竞争共享的资源被同步使用,可能导致死锁和线程饥饿,如果这是可预见的,最好有一种方式来合适地测量这种竞争。即使竞争不会发生,能够断言这种情况也是非常有帮助的。
10+
11+
Next [如何修复性能问题](06-How to Fix Performance Problems.md)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# 如何修复性能问题
2+
3+
大部分软件都可以通过相对小得多的努力,变得比它们刚发布时,在时间上快10到100倍。在市场发布时间的压力下,选择一个简单快速的解决性能问题的方法而非其他方法是聪明而有效率的。然而,性能是可用性的一部分,而且通常它也需要被更仔细地考虑。
4+
5+
提高一个非常复杂的系统的性能的关键是,充分分析它,以发现“瓶颈”,或者资源耗费的地方。优化一个只占用1%执行时间的函数是没有多大意义的。一个简要的原则是,你在做任何事情之前必须仔细思考,除非你认为它能够使系统或者它的一个重要部分至少快两倍。通常会有一种方法来达到这个效果。考虑你的修改会带来的测试以及质量保证的工作需要。每个修改带来一个测试负担,所以最好这个修改能带来一点大的优化。
6+
7+
当你在某个方面做了一个两倍提升后,你需要至少重新考虑并且可能重新分析,去发现系统中下一个最昂贵的瓶颈,并且攻破那个瓶颈,得到下一个两倍提升。
8+
9+
通常,性能的瓶颈的一个例子是,数牛的数目:通过数脚的数量然后除以4,还是数头的数量。举些例子,我曾犯过的一些错误:没能在关系数据库中,为我经常查询的那一列提供适当的索引,这可能会使得它至少慢了20倍。其他例子还包括在循环里做不必要的I/O操作,留下不再需要的调试语句,不再需要的内存分配,还有,尤其是,不专业地使用库和其他的没有为性能充分编写过的子系统。这种提升有时候被叫做“低垂的水果”,意思是它可以被轻易地获取,然后产生巨大的好处。
10+
11+
你在用完这些“低垂的水果”之后,应该做些什么呢?你可以爬高一点,或者把树锯倒。你可以继续做小的改进或者你可以严肃地重构整个系统或者一个子系统。(不只是在新的设计里,在信任你的boss这方面,作为一个好的程序员,这是一个非常好的使用你的技能的机会)然而,在你考虑重构子系统之前,你应该问你自己,你的建议是否会让它好五倍到十倍。
12+
13+
Next [如何优化循环](07-How to Optimize Loops.md)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# 如何优化循环
2+
3+
有时候你会遇到循环,或者递归函数,它们会花费很长的执行时间,可能是你的产品的瓶颈。在你尝试使循环变得快一点之前,花几分钟考虑是否有可能把它整个移除掉,有没有一个不同的算法?你可以在计算时做一些其他的事情吗?如果你不能找到一个方法去绕开它,你可以优化这个循环了。这是很简单的,move stuff out。最后,这不仅需要独创性而且需要理解每一种语句和表达式的开销。这里是一些建议:
4+
5+
- 删除浮点运算操作。
6+
- 非必要时不要分配新的内存。
7+
- 把常量都放在一起声明。
8+
- 把I/O放在缓冲里做。
9+
- 尽量不使用除法。
10+
- 尽量不适用昂贵的类型转换。
11+
- 移动指针而非重新计算索引。
12+
13+
这些操作的具体代价取决于你的具体系统。在一些系统中,编译器和硬件会为你做一些事情。但必须清楚,有效的代码比需要在特殊平台下理解的代码要好。
14+
15+
Next [如何处理I/O开销](08-How to Deal with IO Expense.md)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# 如何处理I/O代价
2+
3+
在很多问题上,处理器的速度比硬件交流要快得多。这种代价通常是小的I/O,可能包括网络消耗,磁盘I/O,数据库查询,文件I/O,还有其他与处理器不太接近的硬件使用。所以构建一个快速的系统通常是一个提高I/O的问题,而非在紧凑的循环里优化代码或者甚至优化算法。
4+
5+
有两种基本的技术来优化I/O:缓存和代表(译者注:比如用短的字符代表长的字符)。缓存是通过本地存储数据的副本,再次获取数据时就不需要执行I/O,以此来避免I/O(通常避免读取一些抽象的值)。缓存的关键在于让(上层对于)哪些数据是主干的,哪些数据是副本,完全透明。主干的数据只有一份-周期。缓存有这样一种危险:副本有时候不能立刻反映主干的修改。
6+
7+
代表是通过更高效地表示数据来让I/O更廉价。这通常会限制其他的要求,比如可读性和可移植性。
8+
9+
代表通常可以用他们第一实现中的两到三个因子来做优化。实现这点的技术包括使用二进制表示而非人类可识别的方式,传递数据的同时也传递一个符号表,这样长的符号就不需要被编码,极端的,可能会像哈弗曼编码。
10+
11+
一个偶尔可行的第三方技术是让计算更接近数据,来优化本地引用。例如,如果你正在从数据库读取一些数据并且在它上面执行一些简单的计算,比如求和,试着让数据库服务器去做这件事,这高度依赖于你正在工作的系统的类型,但这个方面你必须自己探索。
12+
13+
Next [如何管理内存](09-How to Manage Memory.md)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# 如何管理内存
2+
3+
内存是一种你不可以耗尽的珍贵资源。在一段时期里,你可以无视它,但最终你必须决定如何管理内存。
4+
5+
堆内存是在单一子程序范围外,需要持续(保留)的空间。一大块内存,在没有东西指向它的时候,是无用的,因此被称为*垃圾*。根据你所使用的系统的不同,你可能需要自己显式释放将要变成垃圾的内存。更多时候你可能使用一个有*垃圾回收器*的系统。一个垃圾回收器会自己注意到垃圾的存在并且在不需要程序员做任何事情的情况下释放它的内存空间。垃圾回收器是奇妙的:它减小了错误,然后增加了代码的简洁性。如果可以的话,使用垃圾回收器。
6+
但是即使有了垃圾回收机制,你还是可能把所有的内存填满垃圾。一个典型的错误是把哈希表作为一个缓存,但是忘了删除对哈希表的引用。因为引用仍然存在,被引用者是不可回收但却无用的。这就叫做*内存泄露*。你应该尽早发现并且修复内存泄露。如果你会长时间运行系统,内存可能在测试中不会被耗尽,但可能在用户那里被耗尽。
7+
8+
创建新对象在任何系统里都是有点昂贵的。然而,在子程序里直接为局部变量分配内存通常很便宜,因为释放它的策略很简单。你应该避免不必要的对象创建。
9+
10+
当你可以定义你一次需要的数量的上界的时候,一个重要的情况出现了:如果这些对象都占用相同大小的内存,你可以使用单独的一块内存,或缓存,来持有所有的这些对象。你需要的对象可以在这个缓存里以循环的方式分配和释放,所以它有时候被称为环缓存。这通常比堆内存分配更快。(译者注:这也被称为对象池。)
11+
12+
有时候你需要显式释放已分配的内存,所以它可以被重新分配而非依赖于垃圾回收机制。然后你必须在每块内存上使用谨慎的智慧,并且为它设计一种在合适的时候重新分配的方式。这种销毁的方式可能随着你创建的对象的不同而不同。你必须确定每个内存分配方法的执行与最终都匹配一个内存释放操作。(译者注:在C里面,no malloc no free,在C++里面,no new no free)。这通常是很困难的,所以程序员通常会实现一种简单的方式或者垃圾回收机制,比如引用计数,来为它们做这件事情。
13+
14+
Next [如何处理偶现的Bug](10-How to Deal with Intermittent Bugs.md)

0 commit comments

Comments
 (0)