Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: first draft #142

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/HowToBeAProgrammer.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions LANGS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
* [English](en/)
* [Chinese](zh/)
* [Japanese](jp/)
* [Chinese-Traditional](zh-traditional/)
21 changes: 21 additions & 0 deletions zh-traditional/1-Beginner/Personal-Skills/01-Learn-To-Debug.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 學習除錯


[//]: # (Version:1.0.0)
除錯是成為程式設計師的基石。除錯這個詞第一個含義是移除錯誤,但真正有意義的含義是透過檢查來觀察程式的運行。一個不會除錯的程式設計師等同於盲人。

理想主義者認為設計、分析、複雜的理論或其他東西比實際除錯更基本,他們不是現實的程式設計師。現實的程式設計師不會活在理想的世界裡。即使你是完美的,你也需要與你周圍主要的軟體公司或組織的程式碼,和你同事寫的程式碼打交道。這些程式碼和它們的文件大多數都是不完美的。如果不能洞察程式碼的具體執行過程,最小的変動都會把你永遠地拋出去。通常這種可見性只能從實驗獲得,也就是除錯。

除錯是與程式運行相關的事情,而非與程式本身相關。你從主要的軟體公司購買一些產品,通常不會看到產品背後的程式碼。但程式碼不遵循文件的情況或文件沒有說明的情況仍然會出現。更常見的是,你的程式出現了一個錯誤,當你檢查你寫的程式碼的時候,卻不知道這個錯誤是怎麼發生的。這意味著你做的一些假設並不對,或者一些你沒有預料到的情況發生了。有時候,奇妙的修改程式碼的技巧可能會生效。當無效時,你必須除錯。

為了獲得程式執行過程的可見性,你必須能夠執行程式碼並從這個過程中觀察到發生了什麼。有時候這是顯而易見的,比如一些正在呈現在螢幕上的東西,或者兩個事件之間的延遲。在許多其他的案例中,除錯與一些不一定可見的東西相關,比如程式碼中一些變量的狀態,哪一行程式碼正在被執行,或者一些斷言是否持有了一個複雜的資料結構。這些隱藏的細節必須被顯露出來。

觀察一個正在執行程式的內部的方法通常可按如下分類:

- 使用一個除錯工具;
- Logging [(詳情看)](../../4-Glossary.md) - 利用打印程式句子顯示變數值,找出流程出錯地方。

當除錯工具穩定可用時,它們是非常美妙的,但 Printlining 和寫日志甚至是更加重要的。除錯工具通常落後於編程語言的發展,所以在某些時候它們都可能是無效的。另外,除錯工具可能輕微改變程式實際執行的方式。最后,除錯有許多種,比如檢查一個斷言和一個巨大的資料結構,這需要寫程式碼並改變程式的運行。當除錯工具可用時,知道如何使用除錯工具是一件好事,但學會使用其他兩種方式也是至關重要的。

當除錯需要修改程式碼的時候,一些初學者會感到害怕。這是可以理解的,這有點像探索性外科手術。但你需要學會打破程式碼,讓它跳起來,你需要學會在它上面做實驗,並需要知道你臨時對它做的任何事情都不會使它變得更糟。如果你感受到了這份恐懼,找一位導師 - 正是因為許多人在一開始面對這種恐懼的時候表現的太脆弱,我們因此失去了很多本可以變成優秀程式設計師的人。
Next [如何通过分离问题空间来 Debug](02-How-to-Debug-by-Splitting-the-Problem-Space.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 如何通过分割问题 Debug
[//]: # (Version:1.0.0)
调试是有趣的,因为它一开始是个迷。你认为它应该这样做,但实际上它却那样做。很多时候并不仅是这么简单---我给出的任何例子都会被设计来与一些偶尔在现实中会发生的情况相比较。调试需要创造力与智谋。如果说调试有简单之道,那就是在这个谜题上使用分治法。

假如,你创建了一个程序,它会依次执行十件事情。当你运行它的时候,它却崩溃了。但你本来的目的并不是想让它崩溃,所以现在一个谜题扔给你了。当你查看输出时,你可以看到序列里前七件事情运行成功了。最后三件事情在输出里却看不到,所以你的谜题变小了:“它是在执行第8、9、10件事的时候崩溃的”。

你是否可以设计一个实验来观察它是在哪件事情上崩溃呢?当然,你可以用一个调试器或者我们可以在第8第9件事后面加一些[printlining](../../4-Glossary.md)的语句(或者你正在使用的任何语言里的等价的事情),当我们重新运行它的时候,我们的谜题会变得更小,比如“它是在做第九件事的时候崩溃的”。我发现,把谜题是怎样的一直清楚地记在心里能让我们保持注意力。当几个人在一个问题的压力下一起工作时,很容易忘记最重要的谜题是什么。

调试技术中分治的关键和算法设计里的分治是一样的。你只要从中间开始划分,就不用划分太多次,并且能快速地调试。但问题的中点在哪里?这就是真正需要创造力和经验的地方了。

对于一个真正的初学者来说,可能发生错误的地方好像在代码的每一行里都有。一开始,你看不到一些你稍后开发的时候才会看到的其它纬度,比如执行过的代码段,数据结构,内存管理,与外部代码的交互,一些有风险的代码,一些简单的代码。对于一个有经验的程序员,这些其他的维度为整个可能出错的事情展示了一个不完美但是有用的思维模型。拥有这样的思维模型能让一个人更高效地找到谜题的中点。

一旦你最终划分出了所有可能出错的地方,你必须试着判断错误躲在哪个地方。比如:这样一个谜题,哪一行未知的代码让我的程序崩溃了?你可以这样问自己,出错的代码是在我刚才执行的程序中间的那行代码的前面还是后面?通常你不会那么幸运就能知道错误在哪行代码甚至是哪个代码块。通常谜题更像这个样子的:“图中的一个指针指向了错误的结点还是我的算法里变量自增的代码没有生效?”,在这种情况下你需要写一个小程序去确认图中的指针是否都是对的,来决定分治后的哪个部分可以被排除。

Next [如何移除错误](03-How-to-Remove-an-Error.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 如何移除一个错误
[//]: # (Version:1.0.0)
我曾有意把检查程序执行和修复错误分割开来,但是当然,调试也意味着移除 bug。理想状况下,当你完美的发现了错误以及它的修复方法时,你会对代码有完美的理解,并且有一种顿悟(啊哈!)的感觉。但由于你的程序会经常使用其他不具有可视性的、没有一致性注释的系统(比如第三方库),所以完美是不可能的。在其他情况下,可能代码是如此的复杂以至于你的理解可能并不完美。

在修复 bug 时,你可能想要做最小的改变来修复它。你可能看到一些其他需要改进的东西,但不会同时去改进他们。请使用科学的方法去改进一个东西,并且一次只改变一个东西。修复 bug 最好的方式是能够重现 bug,然后把你的修复替换进去,重新运行你的程序,观察,直到 bug 不再出现。当然,有时候不止一行代码需要修改,但你在逻辑上仍然需要使用一个独立原子(译者注:以前人们认为原子不可再分,所以用用原子来代表不可再分的东西)的改变来修复这个 bug。

有时候,可能实际上有几个 bug,但表现出来好像是一个。这取决于你怎么定义 bug,你需要一个一个地修复它们。有时候,程序应该做什么或者原始作者想要做什么是不清晰的。在这种情况下,你必须多加练习,增加经验,评判并为代码赋予你自己的认知。决定它应该做什么,并注释或用其他方式阐述清楚,然后修改代码以遵循你赋予的含义。这是一个进阶或高级的技能,有时甚至比一开始用原始的方式创建这些代码还难,但真实的世界经常是混乱的。你必须修复一个你不能重写的系统。

Next [如何使用日志调试](04-How-to-Debug-Using-a-Log.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 如何使用日志调试
[//]: # (Version:1.0.0)
*Logging*(日志)是一种编写系统的方式,可以产生一系列信息记录,被称为 log。*Printlining* 只是输出简单的,通常是临时的日志。初学者一定要理解并且使用日志,因为他们对编程的理解是局限的。因为系统的复杂性,系统架构必须理解与使用日志。在理想的状态下,程序运行时产生的日志信息数量需要是可配置的。通常,日志提供了下面三个基本的优点:

- 日志可以提供一些难以重现的 bug 的有效信息,比如在产品环境中发生的、不能在测试环境重现的 bug。
- 日志可以提供统计和与性能相关的数据,比如语句间流逝过的时间。
- 可配置的情况下,日志允许我们获取普通的信息,使得我们可以在不修改或重新部署代码的情况下调试以处理具体的问题。

需要输出的日志数量总是一个简约与信息量的权衡。太多的信息会使得日志变得昂贵,并且造成[*滚动目盲*](../../4-Glossary.md),使得发现你想要的信息变得很困难。但信息太少的话,日志可能不包含你需要的信息。出于这个原因,让日志的输出可配置是非常有用的。通常,日志中的每个记录会标记它在源代码里的位置,执行它的线程(如果可用的话),时间精度,并且通常还有一些额外的有效信息,比如一些变量的值,剩余内存大小,数据对象的数量,等等。这些日志语句撒遍源码,但只出现在主要的功能点和一些可能出现危机的代码里。每个语句可以被赋予一个等级,并且只有在系统被配置成输出相应等级的记录的时候才输出这个等级的记录。你应该设计好日志语句来标记你预期的问题。预估测量程序表现的必要性。

如果你有一个永久的日志,printling 现在可以用日志的形式来完成,并且一些调试语句可能会永久地加入日志系统。

Next [如何理解性能问题](05-How-to-Understand-Performance-Problems.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# 如何理解性能问题
[//]: # (Version:1.0.0)
学习理解运行的程序的性能问题与学习 debug 是一样不可避免的。即使你完美、精确地理解了你的代码运行时所产生的开销,你的代码也会调用其他你几乎不能控制的或者几乎不可看透的软件系统。然而,实际上,通常性能问题和调试有点不一样,而且往往要更简单些。

假如你或你的客户认为你的一个系统或子系统运行太慢了。在你把它变快之前,你必须构建一个它为什么慢的思维模型。为了做到这个,你可以使用一个图表工具或者一个好的日志,去发现时间或资源真正被花费在什么地方。有一句很有名的格言:90%的时间会花费在10%的代码上。在性能这个话题上,我想补充的是输入输出开销的重要性。通常大部分时间是以某种形式花费在 I/O 上。发现昂贵的 I/O 和昂贵的10%代码是构建思维模型的一个好的开始。

计算机系统的性能有很多个维度,很多资源会被消耗。第一种资源是“挂钟时间”,即执行程序的所有时间。记录“挂钟时间”是一件特别有价值的事情,因为它可以告诉我们一些图表工具表现不了的不可预知的情况。然而,这并不总是描绘了整幅图景。有时候有些东西只是稍微多花费了一点点时间,并且不会引爆什么问题,所以在你真实要处理的计算机环境中,多一些处理器时间可能会是更好的选择。相似的,内存,网络带宽,数据库或其他服务器访问,可能最后都比处理器时间要更加昂贵。

竞争共享的资源被同步使用,可能导致死锁和互斥。死锁是由于不恰当的同步和请求资源导致线程执行能力的丧失。互斥是对于资源访问的不恰当安排。如果这是可以预料到的,最好在你的项目开始前就采取措施来地衡量线程争抢。即使线程争抢不会发生,对于有效维护它们也是很有帮助的。

Next [如何修复性能问题](06-How-to-Fix-Performance-Problems.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 如何修复性能问题
[//]: # (Version:1.0.0)
大部分软件都可以通过付出相对较小的努力,让他们比刚发布时快上10到100倍。在市场的压力下,选择一个简单而快速的解决问题的方法是比选择其它方法更为明智而有效率的选择。然而,性能是可用性的一部分,而且通常它也需要被更仔细地考虑。

提高一个非常复杂的系统的性能的关键是,充分分析它,来发现其“瓶颈”,或者其资源耗费的地方。优化一个只占用1%执行时间的函数是没有多大意义的。一个简要的原则是,你在做任何事情之前必须仔细思考,除非你认为它能够使系统或者它的一个重要部分至少快两倍。通常会有一种方法来达到这个效果。考虑你的修改会带来的测试以及质量保证的工作需要。每个修改带来一个测试负担,所以最好这个修改能带来一点大的优化。

当你在某个方面做了一个两倍提升后,你需要至少重新考虑并且可能重新分析,去发现系统中下一个最昂贵的瓶颈,并且攻破那个瓶颈,得到下一个两倍提升。

通常,性能的瓶颈的一个例子是,数牛的数目:通过数脚的数量然后除以4,还是数头的数量。举些例子,我曾犯过的一些错误:没能在关系数据库中,为我经常查询的那一列提供适当的索引,这可能会使得它至少慢了20倍。其他例子还包括在循环里做不必要的 I/O 操作,留下不再需要的调试语句,不再需要的内存分配,还有,尤其是,不专业地使用库和其他的没有为性能充分编写过的子系统。这种提升有时候被叫做“低垂的水果”,意思是它可以被轻易地获取,然后产生巨大的好处。

你在用完这些“低垂的水果”之后,应该做些什么呢?你可以爬高一点,或者把树锯倒。你可以继续做小的改进或者你可以严肃地重构整个系统或者一个子系统。(不只是在新的设计里,在信任你的 boss 这方面,作为一个好的程序员,这是一个非常好的使用你的技能的机会)然而,在你考虑重构子系统之前,你应该问你自己,你的建议是否会让它好五倍到十倍。

Next [如何优化循环](07-How-to-Optimize-Loops.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 如何优化循环
[//]: # (Version:1.0.0)
有时候你会遇到循环,或者递归函数,它们会花费很长的执行时间,可能是你的产品的瓶颈。在你尝试使循环变得快一点之前,花几分钟考虑是否有可能把它整个移除掉,有没有一个不同的算法?你可以在计算时做一些其他的事情吗?如果你不能找到一个方法去绕开它,你可以优化这个循环了。这是很简单的,move stuff out。最后,这不仅需要智慧而且需要理解每一种语句和表达式的开销。这里是一些建议:

- 删除浮点运算操作。
- 非必要时不要分配新的内存。
- 把常量都放在一起声明。
- 把 I/O 放在缓冲里做。
- 尽量不使用除法。
- 尽量不适用昂贵的类型转换。
- 移动指针而非重新计算索引。

这些操作的具体代价取决于你的具体系统。在一些系统中,编译器和硬件会为你做一些事情。但必须清楚,有效的代码比需要在特殊平台下理解的代码要好。

Next [如何处理I/O开销](08-How-to-Deal-with-IO-Expense.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 如何处理I/O代价
[//]: # (Version:1.0.0)
在很多问题上,处理器的速度比硬件交流要快得多。这种代价通常是小的 I/O,可能包括网络消耗,磁盘 I/O,数据库查询,文件 I/O,还有其他与处理器不太接近的硬件使用。所以构建一个快速的系统通常是一个提高 I/O,而非在紧凑的循环里优化代码或者甚至优化算法的问题。

有两种基本的技术来优化 I/O:缓存和代表(译者注:比如用短的字符代表长的字符)。缓存是通过本地存储数据的副本,再次获取数据时就不需要再执行 I/O,以此来避免 I/O(通常避免读取一些抽象的值)。缓存的关键在于要让哪些数据是主干的,哪些数据是副本变得显而易见。主干的数据只有一份(在一个更新周期里)。缓存有这样一种危险:副本有时候不能立刻反映主干的修改。

代表是通过更高效地表示数据来让 I/O 更廉价。这通常会限制其他的要求,比如可读性和可移植性。

代表通常可以用他们第一实现中的两到三个因子来做优化。实现这点的技术包括使用二进制表示而非人类可识别的方式,传递数据的同时也传递一个符号表,这样长的符号就不需要被编码,一个极端的例子是哈弗曼编码。

另一种有时能够用来优化本地引用的技术是让计算更接近数据。例如,如果你正在从数据库读取一些数据并且在它上面执行一些简单的计算,比如求和,试着让数据库服务器去做这件事,这高度依赖于你正在工作的系统的类型,但这个方面你必须自己探索。

Next [如何管理内存](09-How-to-Manage-Memory.md)
Loading