标题: [翻译历史Linux邮件列表] 在 Linux 内核代码中使用 goto:“删除 goto 是一种宗教选择,而不是技术选择。”
时间: 2022-05-13发布,2022-05-13修改
来源:https://web.archive.org/web/20130521051957/http:/kerneltrap.org/node/553/2131
最近在 lkml 上有一个关于在 Linux 内核代码中频繁使用“goto”的讨论。也许是借鉴了 Edsger Dikjstra 1968 年发表的题为《Goto结构有害论(Go To Statement Considered Harmful)》的论文,该论点提出使用 goto 只会产生“意大利面条式代码”。该理论的最新支持者是 Niklaus Wirth,他在 1970 年左右开发了 Pascal,在 1979 年开发了其继任者 Modula-2。
lkml 里近期的讨论非常清楚地表明在 Linux 内核代码中使用 goto 是经过深思熟虑的合理选择。在O'Reilly 的优秀著作《Linux 设备驱动程序》的第 2 章中也可以找到这样的解释。包括 Robert Love(采访)、Rik van Riel(采访)和 Linux 创始人 Linus Torvalds 在内的许多内核开发人员在以下讨论中进一步解释了这一点。
Linus,
我真的反对在任何不需要“goto”的代码中使用“goto”这个关键字。当然,我是一个linux内核新手,所以我没有发言权。
让我试着说一下我会如何更改下面的代码片段:
引用:2003-01-12 14:15, 来自 Linus Torvalds:
if (spin_trylock(&tty_lock.lock))
goto got_lock;
if (tsk == tty_lock.lock_owner) {
WARN_ON(!tty_lock.lock_count);
tty_lock.lock_count++;
return flags;
}
spin_lock(&tty_lock.lock);
got_lock:
WARN_ON(tty_lock.lock_owner);
我会这样修改(尚未实际编译测试是否有效):
if (!(spin_trylock(&tty_lock.lock))) {
if (tsk ==tty_lock.lock_owner) {
WRAN_ON(!tty_lock.lcok_count);
tty_lock.lock_count++;
return flags;
}
}
WARN_ON(tty_lock.lock_owner);
上述代码应该可以实现相同的功能,并且不会像“goto”那样产生意大利面条式代码,对不对?“goto”很糟糕,非常非常糟糕。另请注意,上面的两个 if 语句甚至可以通过在 if 中使用短路 &&
进一步组合成一个语句。
如果我误解了原始代码,请原谅我……我刚看到一个“goto”就已经喘不过气来。总有比“goto”更好的选择。
- Rob
我真的反对在任何不需要“goto”的代码中使用“goto”这个关键字。
我认为 goto 很好,而且它通常比大量缩进更具可读性。如果代码流实际上不是自然缩进,则尤其如此(在这种情况下是这样,另一些情况可能不是。所以我并不认为使用 goto 总是比不使用它更清晰,但总的来说 goto 对可读性来说非常好)。
当然,在像 Pascal 这样的愚蠢语言中,标签不能是描述性的,goto 可能很糟糕。但这不是 goto 的错,这是语言设计者的脑残。
- Linus
我认为 goto 很好
你是一个相对成功的人,所以我想我不应该与你进行风格上的争论。
然而,我一直被教导,并且一直相信“goto”本质上是邪恶的,是造成意大利面条式代码的罪魁祸首。在编写代码几个月或几年以后,你就不得不通读代码才能理解它。它会让你突然跳到完全不相关的地方,然后向后跳到另一个地方,代码很快就会变得丑陋,这使以后的代码调试完全陷入地狱。
将那部分代码改为不用 goto 而用类似我写的那些东西对您来说会很糟糕吗?不要介意哲学论点,我只是在谈论一段相对较小的代码的良好编码风格。
如果您愿意,完全可以在您的代码中添加注释以有意义地描述正在发生的事情,而不是依赖 goto 标签。
一般来说,如果你能正确地构建你的代码,你就永远不需要 goto,如果你不需要 goto,你就不应该使用它。正如我一直被教导的那样,这只是“常识”。除非您有意尝试编写其他人难以阅读的代码。
- Rob
然而,我一直被教导,并且一直相信“goto”本质上是邪恶的,是造成意大利面条式代码的罪魁祸首。
不,你被搞计算机科学的那帮人洗脑了,他们认为 Niklaus Wirth 实际上知道自己在说什么,但 Niklaus Wirth 不知道自己在说什么,他根本不明白。
在编写代码几个月或几年以后,你就不得不通读代码才能理解它。它会让你突然跳到完全不相关的地方,然后向后跳到另一个地方,代码很快就会变得丑陋,这使以后的代码调试完全陷入地狱。
任何 if 语句都是 goto,就像所有结构化循环一样。
所以有时结构化很好。当它好的时候,你应该使用它。
有时结构化不好,会碍事,使用“goto”更加清晰。
例如,代码中存在不嵌套的条件是很常见的。
在这种情况下,您有两种选择:
这使得代码更具可读性,因为代码只是按照算法所说的去做。
这通常会使代码变得更长、更不可读、更难维护。
Pascal 语言是后一个问题的典型受害者,因为它没有“break”语句,所以标准 Pascal 语言里的循环最终看起来一团糟,因为你必须添加完全任意的逻辑来表示“我现在完成了”。
- Linus
不,你被搞计算机科学的那帮人洗脑了,他们认为 Niklaus Wirth 实际上知道自己在说什么,但 Niklaus Wirth 不知道自己在说什么,他根本不明白。
我想是 Edsger Dijkstra 在他的结构化编程推广行动中创造了“goto是邪恶的”这一观点?
尽管如此,他和 Niklaus Wirth 都错了……
- Robert Love
我想是 Edsger Dijkstra 在他的结构化编程推广行动中创造了“goto是邪恶的”这一观点?
是的,他错了,但他已经不在了,我们不该说已故之人的坏话。所以这些天我只能抱怨 Niklaus Wirth,他把“结构化编程”的东西用他的语言(Pascal 和 Modula-2)强制执行,从而把他的邪恶强加给了无数代不得不学习这些语言的计算机专业学生,这实际上对实际工作没有用。
(是的,是的,Pascal 的大多数实用版本最终都具有打破结构化所需的所有东西,但正如你可能会说的那样,我是年轻时不得不用“标准 Pascal”写作业的未洗大众之一,我为此伤痕累累)。
- Linus
然而,我一直被教导,并且一直相信“goto”本质上是邪恶的,是造成意大利面条式代码的罪魁祸首。
如果代码的主要流程是通过一堆难以追踪的 goto 实现的,而你选择责怪工具而非程序员,我想你可能会责怪 goto。
然而,goto 也可以成为使代码更具可读性的好工具。恕我直言,goto 语句是在 C 函数中优雅地实现异常处理的一种好方法。也就是说,它可以轻松处理不经常发生的错误情况,但又不会使错误处理代码弄乱主代码的执行路径。
比如,你可以看这个 fs/super.c::dokernmount() 函数:
mnt = alloc_vfsmnt(name);
if (!mnt)
goto out;
sb = type->get_sb(type, flags, name, data);
if (IS_ERR(sb))
goto out_mnt;
你看到把用于错误处理的清理代码移到主流程之外是如何让代码变得更易读的了吗?
祝好,
Rik
将那部分代码改为不用 goto 而用类似我写的那些东西对您来说会很糟糕吗?不要介意哲学论点,我只是在谈论一段相对较小的代码的良好编码风格。
如果您愿意,完全可以在您的代码中添加注释以有意义地描述正在发生的事情,而不是依赖 goto 标签。
一般来说,如果你能正确地构建你的代码,你就永远不需要 goto,如果你不需要 goto,你就不应该使用它。正如我一直被教导的那样,这只是“常识”。除非您有意尝试编写其他人难以阅读的代码。
在尝试贡献自己的东西之前,我花了一些时间查看内核源代码,对风格和流程有所了解。在大多数情况下,Linux 代码的质量等于或超过我从事的商业产品的质量。它可能并不完美,但我希望维护者专注于功能和错误修复,而不是宗教问题。
你对“goto”的态度可能是基于一篇出色但过时的文章《Goto结构有害论》,该文章由 Edsger W. Dijkstra 撰写,由 ACM 于 1968 年出版。(最近的再版可在 http://www.acm.org/classics/oct95/ 找到。)从日期上可以看出,这篇文章是在现代编程语言和编程习惯形成之前发表的,它来自 Fortran 统治时期,发表在 Fortran 77 提供避免意大利面条式代码的重要工具之前。
“goto”本身并不危险——它是一种语言特性,可以直接转换为机器代码中实现的跳转指令。与指针、运算符重载以及编程中许多其他“可感知”的弊端一样,“goto”被那些受“屎山代码”困扰的人广泛憎恨。糟糕的代码是糟糕程序员的产物——根据我的经验,无论“goto”是否可用,糟糕的程序员始终会编写出糟糕的代码。
如果你认为人们不能用没有“goto”的语言编写意大利面条式代码,我可以给你发一些可爱的示例来消除你的这种想法。;)
通过在短距离内使用有据可查的标签,“goto”可以比一系列复杂的标志变量或其他结构更有效、更快、更清晰。“goto”也可能比替代方法更安全、更直观。“break”是“goto”;“continue”是“goto”——这些都是显式移动执行点的语句。
也就是说,自从我抛弃 BASIC 和 Fortran IV 之后,我在 C、C++、Fortran 95 和 COBOL(你没看错)等项目中都使用过一两个“goto”语句。在一个案例中,单个“goto”使时间关键型应用程序的速度提高了一倍;在另一个案例中,“goto”将一段代码缩短一半并使算法更加清晰。我不会为了好玩而随意使用 goto —— 除非我参加的是代码混淆竞赛 ;)
现在看来,我们一直在降低技术实力的标准:如果某些东西有可能被“错误”使用,高尚的设计师就会删除有问题的语法,而不是寻找或培训有能力的程序员。这就是 Java 移除指针(先不说其他的)的原因——并不是指针没有用或没有效率,而是他们需要维持程序员的纪律。
有些东西虽然是教条,但并不意味着它是绝对真理。如果有什么区别的话,那就是对教条的嗅闻应该相当仔细,因为如果你足够接近它,就会发现它往往充斥着阶级意味。删除 goto 是一种宗教选择,而不是技术选择。
我可以将其与一般社会中的愚蠢法律相提并论,但这些对于这个邮件列表来说已经超出讨论范围了。
- Scott
另一段讨论
我只编译过(并没有测试过这段代码),但它应该比原始代码快得多。为什么?因为我们在每次调用 open 时都会在代码中的多个位置消除额外的“跳转”。是的,它的代码更多,所以内核更大一些,但同时它应该更快,而且现在内存应该不再是问题。
Rob,因为你有一段时间没关注这个邮件列表,所以你可能没注意到,当前这代计算机里小就意味着快,而许多久负盛名的性能技巧(用空间换时间)反而会造成性能损失。
这可以用 gcc 的 -O2
和 -Os
编译优化参数来验证,实际上 -Os
通常比 -O2
更快。(译者注:除了不启用会增加可执行文件大小的优化之外,-Os
启用 -O2
中的其余所有优化。也就是说,-Os
比 -O2
启用的优化更少,但因为生成的可执行文件更小,所以通常反而更快。)
这是因为并非所有内存都是平等的,与CPU缓存相比,主内存非常慢,因此稍微大一点的代码会导致更多的缓存未命中,所以速度会变慢,即使执行的命令显著减少也无法平衡这个损失。(译者注:该结论直到2022年都非常正确,比如AMD的5800X3D就是靠堆CPU缓存显著提升了游戏性能。)
此外,性能损失通常是间接的。即,你正在更改的代码没有明显的性能差异,但是更改会使其他代码变慢,因为当前代码变大,导致其他代码从缓存中被逐出。
不幸的是,虽然这种影响是已知的,但什么时候该优化空间、什么时候该优化CPU周期,规则并不是很清楚。就算是在同一系列的CPU中,不同CPU型号的最佳选择也不尽相同。
如果你用谷歌搜一下-Os
,应该可以在去年的邮件列表中找到关于该话题的几个讨论。
- David Lang
这是因为并非所有内存都是平等的
“并非所有内存都是平等的”还有另一种情况,比如,我在使用多路CPU服务器。
(译者注:多路CPU服务器中,不同的CPU直连到不同的内存模组。与CPU直连的内存,读写速度比非直连内存快得多,因为读写非直连内存需要靠其他CPU中转。)
- Bill
我只编译过(并没有测试过这段代码),但它应该比原始代码快得多。为什么?因为我们在每次调用 open 时都会在代码中的多个位置消除额外的“跳转”。是的,它的代码更多,所以内核更大一些,但同时它应该更快,而且现在内存应该不再是问题。
如果你想应用它,这里是补丁(我只编译测试过,我还没有启动它)。这个补丁适用于 2.5.56 内核。
--- open.c.orig 2003-01-12 16:17:01.000000000 -0500
+++ open.c 2003-01-12 16:22:32.000000000 -0500
@@ -100,44 +100,58 @@
error = -EINVAL;
if (length)
- goto out;
+ return error;
……
请别做这种操作。等下次锁定发生变化并且此处需要加锁的时候,某些可怜人就不得不把这些代码再次改回 goto
。可能这里不会出现这种情况,但作为一般规则,不要这样做。这是我的经验之谈。
至于效率,那是编译器的工作。
祝好
Oliver
请别做这种操作。等下次锁定发生变化并且此处需要加锁的时候,某些可怜人就不得不把这些代码再次改回
goto
。可能这里不会出现这种情况,但作为一般规则,不要这样做。这是我的经验之谈。
至于效率,那是编译器的工作。
我会说“请不要使用 goto”,而是加一个“cleanup_lock”函数并将其添加到所有 return 语句之前。它不应该是一个负担。是的,它要求开发人员更加努力地工作,但最终结果是更好的代码。
- Rob
On Sun, 2003-01-12 at 17:22, Rob Wilkens wrote:
我会说“请不要使用 goto”,而是加一个“cleanup_lock”函数并将其添加到所有 return 语句之前。它不应该是一个负担。是的,它要求开发人员更加努力地工作,但最终结果是更好的代码。
不,它很恶心,而且会使内核膨胀。它为 N 个错误路径内联了一堆垃圾,而不是在末尾有一次退出代码。
缓存足迹是关键,你刚刚杀死了它。
而且这样写也并没有变得更易读。
作为最后一个论点,你的代码并没有让我们干净利落地实现通常意义上的堆栈式装卸,即:
do A
if (error)
goto out_a;
do B
if (error)
goto out_b;
do C
if (error)
goto out_c;
goto out;
out_c:
undo C
out_b:
undo B
out_a:
undo A
out:
return ret;
所以是时候停下来了。
- Robert Love
译者注:“堆栈式装卸”(stack-esque wind and unwind)是指始终保持用与打开资源相反的顺序释放资源,如果在某个步骤出错,只释放在它之前打开的资源。在C和C++里,goto是实现这一目标的最佳方法,其他方法要么不高效,要么不易读,要么两者皆是。
『回复列表(14|隐藏机器人聊天)』
https://github.com/NVIDIA/open-gpu-kernel-modules/pull/3#issuecomment-1124292877
该邮件对于理解linux内核驱动程序中的goto使用非常重要。
相关评论翻译
可调整大小的基址寄存器支持是一种 PCIe 扩展,允许调整 PCIe 设备的可映射内存/寄存器空间……
感谢 sjkelly 的贡献,并祝贺此存储库中出现首个拉取请求 :) 我需要了解一下 NVIDIA 中 Resizable BAR 支持的历史,我或者来自 NVIDIA 的其他人将很快回应这个更改。再次感谢!
代码中使用 goto
让我感到不快。
在拉取请求中,我们必须使我们的代码适应当前代码库的编程风格。重写这样的事情与这个拉取请求无关。
我不是说要重写,而是不提倡在新代码中引入更多的 goto
。
嗯,这个补丁确实增加了 3 个 goto。
但是文件中已经有 30 个 goto 了,Linus 自己也说 goto 是很好的内核编码风格,我看没问题。
https://web.archive.org/web/20130521051957/http:/kerneltrap.org/node/553/2131
@TheRawMeatball 看看现有源代码,使用 goto 就是代码库的编程风格。删除 goto 的使用与这个拉取请求无关。
其中一个 goto 不是必需的,因为我们在 rebase 期间删除了其中一个错误路径,所以 @sjkelly 可以删除它。但是除此之外,goto 在 Linux 内核中是相当惯用的。对 goto 的很多仇恨都是基于 Dijkstra 的文章,但那里缺少一些上下文。Dijkstra 从未真正反对本地 goto,而是反对到处乱跳的全局 goto。无论如何,我是否可以建议不要将这个拉取请求变成关于 goto 优点的讨论,以便让 NVIDIA 人员有空间进行适当的审查:)?
使用 GOTO 时最糟糕的部分是其他程序员对它的反应。
https://stackoverflow.com/a/741517
在某些情况下,goto
可以为“真正的”异常处理提供一种替代。
考虑以下代码:
ptr = malloc(size);
if (!ptr)
goto label_fail;
bytes_in = read(f_in,ptr,size);
if (bytes_in =< 0)
goto label_fail;
bytes_out = write(f_out,ptr,bytes_in);
if (bytes_out != bytes_in)
goto label_fail;
显然,这段代码被简化以减少空间占用,所以不要太过在意细节。
但是考虑一下我在生产代码中看到过多次的替代方法,看看编码人员为了避免使用goto
而荒谬的程度:
success = false;
do {
ptr = malloc(size);
if (!ptr)
break;
bytes_in = read(f_in,ptr,size);
if (count =< 0)
break;
bytes_out = write(f_out,ptr,bytes_in);
if (bytes_out != bytes_in)
break;
success = true;
} while (false);
从功能上讲,这两段代码完全相同。实际上,编译器生成的代码几乎完全一致。然而,在热切地安抚 Nogoto(可怕的学术谴责之神)的同时,这位程序员完全打破了循环所代表的基本习惯,并对代码的可读性做了真正糟糕的事情。这并不比直接使用goto
更好。
所以,故事的寓意是,如果你发现自己为了避免使用goto
而不得不采取真正愚蠢的方案,那就应该停下来。
代码看不太懂,但是我的观点是,既然编译以后避免不了jmp等跳转指令,那就在别人写代码用了你政治敏感的指令"goto"的话,你用手段把goto指令去掉 (自动转换成不带goto 但是代码编译后运行逻辑一样的代码),只要编译后的程序还能正常运行就好了。
127.9.147.96
为什么没有人自己设计一个"goto killer" 把所有带goto的代码,都转换成不带goto的代码?
如果可以优雅地转换,我们就不会提出这种批评了:
“删除 goto 是一种宗教选择,而不是技术选择。”
“如果你发现自己为了避免使用goto而不得不采取真正愚蠢的方案,那就应该停下来。”
转换后的代码一定会比原始代码更蠢,因为一切用于“模拟”goto的代码本质上都是goto。if是goto,while是goto,switch是goto,foreach是goto,甚至函数调用实际上也是goto,只是它们用更精美的包装呈现给你了而已。
所以在那些真正需要非结构化跳转的地方,想用更高层次的“结构化跳转”(if/while/函数调用)模拟goto,只能得到愚蠢的代码,甚至根本难以实现,远比不上直接大大方方的使用goto。
因此,就连中毒最深最讨厌goto的人,也没有办法实现这个自动转换器。
@老虎会游泳,C 语言里,怎么使用 goto
更好地组织程序结构,来处理异常呢?
需要实现:
free
该字符串)。目前想法:
int func(char ** err) {
int ret_code = CODE_OK;
char err_buf[256] = "";
snprintf(err_buf, sizeof err_buf / sizeof *err_buf, "读写文件“%s”时出错啦!错误号:%d", "...", errno);
goto _err_io;
_finally:
free(...); // 正常释放
return ret_code;
_err_oom:
ret_code = CODE_NO_MEMORY;
*err = malloc_and_printf("%s", *err_buf == '\0' ? "没内存了" : err_buf);
goto _err_finally;
_err_io:
ret_code = CODE_IO_ERR;
*err = malloc_and_printf("%s", *err_buf == '\0' ? "文件读写出错啦" : err_buf);
goto _err_finally;
_err_finally:
free(...); // 其余对象也释放
goto _finally;
}