V语言评测 (2023)
摘要
本文是对2023年V语言的全面评测,批评了其文档质量、内存管理缺陷以及功能不完整等问题。
<p><a href="https://lobste.rs/s/1rjyeh/v_language_review_2023">评论</a></p>
查看缓存全文
缓存时间: 2026/09/20 02:08
# V 语言评测(2023)
来源:https://n-skvortsov-1997.github.io/reviews/
你发现了一种名为 V 的新编程语言。它看起来不错,网站上有诸多承诺,语法也很优雅,但它实际表现如何呢?本文所描述的一切均针对此提交:`b66447cf11318d5499bd2d797b97b0b3d98c3063`(https://github.com/vlang/v/commit/b66447cf11318d5499bd2d797b97b0b3d98c3063)。本文总结了我使用该语言 6 个月的经验,以及我在撰写本文时从 Discord 上收集到的信息。文章相当长,因为我试图尽可能详细地描述所有内容,以便任何人能够复现相同的行为。
学习新编程语言从哪里开始?没错,从文档开始。V 语言的文档是一个巨大的文件:`docs.md`(https://github.com/vlang/v/blob/master/doc/docs.md)。在开头不远的地方,你可以注意到 V 语言内置的类型。类型 `i128` 和 `u128` 前面的小 `soon` 前缀描述了整个语言的现状。这条说明至少已经在文档中存在 4 年了(`提交`(https://github.com/vlang/v/commit/65a8db85254b8d8d02098843202142e61aa02570)),看来我们还需要再等一段时间。
接着你可能会注意到,与 C 和 Go 不同,`int` 始终是 32 位。但在 0.4.3 版本(https://github.com/vlang/v/releases/tag/0.4.3)中,它现在在 64 位系统上是 64 位,在 32 位系统上是 32 位。你说这只是几处错误,但不,这就是整个 V 文档的现状。开发者如此之少,以至于无法保持文档处于正确状态。文档通常没有描述语言的最重要部分——例如,关于泛型的部分(https://github.com/vlang/v/blob/master/doc/docs.md#generics)只有几个代码示例,没有适当的说明。
诸如下面这样的承诺也常见于文档中:
> 目前,泛型函数定义必须声明其类型参数,但未来 V 将从运行时参数类型中的单字母类型名推断泛型类型参数。
现在向下滚动到最有趣的部分:V 中的内存管理。在现代编程语言中,这几乎是语言最重要的部分。V 提供什么?首先是“垃圾回收”,这是一个很好的选择,大大简化了生活;第二个选择是“arena”,也是一个很好的选择;“手动内存管理”也适用于有经验的程序员。最后也是最有趣的选择是“自动释放”。前两种选择工作相对良好,所以我们来看看后两种。
### 手动内存管理
在此模式下,所有分配都使用 libc 的 malloc 函数,开发者必须自己清理内存。但是标准库函数内部分配的内存呢?让我们看看 `is_ascii` 方法(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/builtin/string.v#L2379C2-L2379C2):
```
@[inline]
pub fn (s string) is_ascii() bool {
return !s.bytes().any(it < u8(` `) || it > u8(`~`))
}
```
这看起来是一个小而安全的函数,但如果你在手动内存管理下调用它,你将会有内存泄漏,因为没有人清理 `bytes()` 方法(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/builtin/string.v#L2040)分配的内存。这里还有更多(https://github.com/vlang/v/blob/master/vlib/builtin/string.v#L822)类似的(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/builtin/string.v#L901)示例(https://github.com/vlang/v/blob/master/vlib/builtin/string.v#L338)。而这仅存在于字符串方法中;在整个标准库中随处可见。好吧,你可以在你的代码中不使用这些函数。让我们看看如果你用 V 在“手动”模式下写一个 Web 服务器会怎样。V 有一个名为 `vweb` 的内置框架。官方示例包含以下代码:
https://github.com/vlang/v/blob/master/examples/vweb/vweb_example.v
我已尽可能简化它:
```
module main
import vweb
struct App {
vweb.Context
}
pub fn (mut app App) index() vweb.Result {
return app.text("Hello World")
}
fn main() {
vweb.run(&App{}, 8082)
}
```
接下来(https://github.com/vlang/v/blob/master/vlib/vweb/vweb.v#L571),数组(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/vweb/vweb.v#L542)在 vweb 代码中从未(https://github.com/vlang/v/blob/master/vlib/vweb/vweb.v#L572)被释放(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/vweb/vweb.v#L958)(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/vweb/vweb.v#L729)。这意味着你在 vweb 上的应用程序将会有内存泄漏。
好吧,并不是每个人都写 Web,也许你需要一个简单的 CLI 工具?不幸的是,标准库中所有用于 CLI 的字符串插值都会分配内存,然后又不清理。从以上所有内容中,我可以得出以下结论:V 中的手动内存管理是一个无法用于实际应用程序的特性。最多只能在简单程序中使用,其中你从头开始编写所有内容,或者内存泄漏对你来说不严重。
### 自动释放
现在我们来谈谈本节中最有趣的部分。让我们看看文档中如何描述此模式:
> 第二种方法是 autofree,可以通过 `-autofree` 启用。它处理大部分对象(约 90-100%):编译器在编译期间自动插入必要的 free 调用。剩余的一小部分对象由 GC 释放。开发人员无需更改代码中的任何内容。“它只是有效”,就像在 Python、Go 或 Java 中一样,只是没有追踪所有事物的繁重 GC 或为每个对象使用昂贵的引用计数(RC)。
令人惊讶的是,他们在每句话中都撒了谎。让我们从头开始,文档向我们保证,使用编译器插入的 `free` 调用,90-100% 将自动清理。考虑到在 Rust 中要实现相同的功能,你需要为编译器提供大量帮助,这听起来相当乐观。V 编译器结果“比 Rust 编译器聪明得多”。
当我在语言仓库的讨论(https://github.com/vlang/v/discussions?discussions_q=is%3Aopen+autofree&page=1)中寻找时,我遇到了一个有趣的评论(链接(https://github.com/vlang/v/discussions/12343#discussioncomment-5828322)):
> 在我的 V 程序中,只有 0.1% 是自动释放的,99.9% 是由垃圾回收器释放的。这完全取决于你正在制作的程序。GC 仍然非常快。
但是,让我们不要把这个当作事实,我们自己试试。这是最简单的代码:
```
module main
struct Data {
data []bool
}
fn main() {
p := Data{
data: [true, false]
}
println(p.data)
}
```
使用 `v -autofree main.v` 编译它。然后运行 `valgrind`:
```
==653065== Memcheck, a memory error detector
==653065== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==653065== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==653065== Command: /test
==653065== [true, false]
==653065==
==653065== HEAP SUMMARY:
==653065== in use at exit: 2 bytes in 1 blocks
==653065== total heap usage: 6 allocs, 5 frees, 1,085 bytes allocated
==653065==
==653065== LEAK SUMMARY:
==653065== definitely lost: 2 bytes in 1 blocks
==653065== indirectly lost: 0 bytes in 0 blocks
==653065== possibly lost: 0 bytes in 0 blocks
==653065== still reachable: 0 bytes in 0 blocks
==653065== suppressed: 0 bytes in 0 blocks
==653065== Rerun with --leak-check=full to see details of leaked memory
==653065==
==653065== For lists of detected and suppressed errors, rerun with: -s
==653065== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
```
出了点问题。同样有趣的是,我们的程序中只有一个数组,但 valgrind 显示分配了 1kb 的内存。请记住这一点,当我们继续讨论网站上关于 V 避免不必要分配的声明时。
让我们编译一个带有 vweb 服务器的简单示例:
```
module main
import vweb
struct App {
vweb.Context
}
pub fn (mut app App) index() vweb.Result {
return app.text("Hello World")
}
fn main() {
vweb.run(&App{}, 8082)
}
```
然后我们用 valgrind 运行它。我没有向服务器发出请求,只是等待了 10 秒钟:
```
==653318== Command: /server
==653318== [Vweb] Running app on http://localhost:8082/
==653318== [Vweb] We have 1 workers
==653318==
==653318== Process terminating with default action of signal 2 (SIGINT)
==653318== at 0x498882D: select (select.c:69)
==653318== by 0x58AAF2: net__select (in /home/skvortsov/server)
==653318== by 0x642D81: net__select_deadline (in /home/skvortsov/server)
==653318== by 0x58B122: net__wait_for_common (in /home/skvortsov/server)
==653318== by 0x58B40B: net__wait_for_read (in /home/skvortsov/server)
==653318== by 0x591D43: net__TcpListener_wait_for_accept (in /home/skvortsov/server)
==653318== by 0x5915F9: net__TcpListener_accept_only (in /home/skvortsov/server)
==653318== by 0x5E57E1: vweb__run_at_T_main_App (in /home/skvortsov/server)
==653318== by 0x5E4262: vweb__run_T_main__App (in /home/skvortsov/server)
==653318== by 0x5F0102: main__main (in /home/skvortsov/server)
==653318== by 0x63F220: main (in /home/skvortsov/server)
==653318==
==653318== HEAP SUMMARY:
==653318== in use at exit: 122,833 bytes in 2,395 blocks
==653318== total heap usage: 2,659 allocs, 264 frees, 541,413 bytes allocated
==653318==
==653318== LEAK SUMMARY:
==653318== definitely lost: 1,004 bytes in 32 blocks
==653318== indirectly lost: 106 bytes in 15 blocks
==653318== possibly lost: 272 bytes in 1 blocks
==653318== still reachable: 121,451 bytes in 2,347 blocks
==653318== suppressed: 0 bytes in 0 blocks
==653318== Rerun with --leak-check=full to see details of leaked memory
==653318==
==653318== For lists of detected and suppressed errors, rerun with: -s
==653318== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
```
没有请求,在 10 秒内我们绝对丢失了 1kb 内存。
让我们回到文档中的描述:
> 剩余的一小部分对象由 GC 释放。
这再次不是事实。我找到了一个最近的提交(https://github.com/vlang/v/commit/207203f5998e6b1844a32fe628e0eb64325db64d),其中传递 `-autofree` 标志会立即将 `gc` 设置为 `none`:
```
'-autofree' {
res.autofree = true
res.gc_mode = .no_gc
res.build_options << arg
}
```
所以,这个说法完全错误。
你知道为什么做了这个改动吗?让我们看一个例子:
```
fn main() {
ptr := malloc(1)
free(ptr)
}
```
然后我们看看 `free` 函数的定义(https://github.com/vlang/v/blob/cc220e60a5a0cc787b68ae357c8ecfd2dc561b6f/vlib/builtin/builtin.c.v#L586):
```
@[unsafe]
pub fn free(ptr voidptr) {
$if prealloc {
return
} $else $if gcboehm ? {
// It is generally better to leave it to Boehm's gc to free things.
// Calling C.GC_FREE(ptr) was tried initially, but does not work
// well with programs that do manual management themselves.
//
// The exception is doing leak detection for manual memory management:
$if gcboehm_leak ? {
unsafe { C.GC_FREE(ptr) }
}
} $else {
C.free(ptr)
}
}
```
`$if`(https://github.com/vlang/v/blob/master/doc/docs.md#if-condition)指定了在编译期间评估的条件。以前,当我们只传递 `-autofree` 而没有明确传递 `-gc none` 时,条件 `$if gcboehm ?` 为真,并且由于 `gcboehm_leak` 默认也未设置,因此 `free` 最终成为一个不执行任何操作的空操作函数。这是生成的 C 代码:
```c
void _v_free(voidptr ptr) {
#if defined(_VPREALLOC)
{
}
#elif defined(_VGCBOEHM)
{
}
#else
{
C.free(ptr);
}
#endif
}
```
所有这些代码都是 C 预处理器代码,所以编译器看到的代码如下:
```c
void _v_free(voidptr ptr) {
}
```
这不会释放任何东西。
让我们回到文档:
> 剩余的一小部分对象由 GC 释放。
这甚至不是真的,即使 V 在某处插入了 `free`,它们也没有任何效果,所有内容都是由 GC 清理的。即使在此修复之后,这也不是真的,因为在传递 `-autofree` 标志时 GC 被禁用了。
你可能看过这个视频:https://www.youtube.com/watch?v=gmB8ea8uLsM
在其中,语言作者展示了他编辑器 Ved(https://github.com/vlang/ved),并展示了如何使用 `v . -autofree` 编译它,并声称他的技术足够成熟,像文本编辑器这样复杂的应用程序不会泄漏。我尝试使用最新版本的 V 并启用 `autofree` 标志构建编辑器,启动二进制文件时得到以下错误:
```
V panic: as cast: cannot cast `map[string]toml.ast.Value` to `[]toml.ast.Value`
v hash: 0966fd3
/tmp/v_1000/ved.5480247081914024169.tmp.c:13797: at _v_panic: Backtrace
/tmp/v_1000/ved.5480247081914024169.tmp.c:14296: by __as_cast
/tmp/v_1000/ved.5480247081914024169.tmp.c:43867: by toml__Doc_value_
/tmp/v_1000/ved.5480247081914024169.tmp.c:43836: by toml__Doc_value
/tmp/v_1000/ved.5480247081914024169.tmp.c:44579: by main__Config_init_colors
/tmp/v_1000/ved.5480247081914024169.tmp.c:44551: by main__Config_reload_config
/tmp/v_1000/ved.5480247081914024169.tmp.c:46595: by main__main
/tmp/v_1000/ved.5480247081914024169.tmp.c:50668: by main
```
没有 `autofree` 一切都工作正常。好吧,显然 autofree 在 3 年里变得更糟了。有趣的是,语言作者自己的项目无法与其语言的主要特性配合工作。
让我们尝试使用 `autofree` 构建编译器本身:
然后让我们尝试使用生成的二进制文件再次编译自身:
我们在运行时得到错误:
```
./v2 self
/tmp/v_1000/v2.10486918756004741764.tmp.c:25123: at string_starts_with: RUNTIME ERROR: invalid memory access
/tmp/v_1000/v2.10486918756004741764.tmp.c:35912: by os__impl_walk_ext
/tmp/v_1000/v2.10486918756004741764.tmp.c:35888: by os__walk_ext
/tmp/v_1000/v2.10486918756004741764.tmp.c:42645: by v__pref__detect_musl
/tmp/v_1000/v2.10486918756004741764.tmp.c:42743: by v__pref__parse_args_and_show_errors
/tmp/v_1000/v2.10486918756004741764.tmp.c:4811: by main__main
/tmp/v_1000/v2.10486918756004741764.tmp.c:5835: by main
```
让我们回到文档的最后部分:
> 开发人员无需更改代码中的任何内容。“它只是有效”,就像在 Python、Go 或 Java 中一样,只是没有追踪所有事物的繁重 GC 或为每个对象使用昂贵的引用计数(RC)。
我们在上面发现,在提交 207203f(https://github.com/vlang/v/commit/207203f5998e6b1844a32fe628e0eb64325db64d)之前,传递 `-autofree` 标志我们**得到**了“追踪所有事物的繁重 GC”,之后即使在最简单的示例中我们也**得到**了内存泄漏。
我还要指出,语言作者早在 `0.3` 版本时(提交(https://github.com/vlang/v/blob/0f9537ece544b7fda31cadf4dc95fd4b552f94be/ROADMAP.md))就承诺使这项技术“可用于生产”,然后是 0.5(https://discord.com/channels/592103645835821068/592106336838352923/1136589637658345563),也许在 0.6(https://discord.com/channels/273534239310479360/818964227783262209/1146427952083513467),以及 ROADMAP(https://github.com/vlang/v/blob/master/ROADMAP.md)中是 1.0。
> “装到成功为止”。
有趣的是,语言作者认为将 `autofree` 与 GC 一起使用没有意义(https://discord.com/channels/592103645835821068/592106336838352923/1126201270902997114),尽管文档说正是 GC 清理了剩余的“10%”对象。真是奇妙。
因此,从以上所有内容可以得出结论,`autofree` 是一项非常粗糙的技术。语言作者试图通过该视频推广它,根据评论来看他成功了,我不明白为什么人们相信他,因为一个简单的测试表明,即使简单的程序也泄漏严重。
**近 5 年后,V 最有趣的功能仍处于非常初级的阶段,而作者除了承诺一切很快就会实现外什么也没做。** 已经很清楚,语言作者及其忠实的追随者会开始说 `autofree` 尚未可用于生产,但我所描述的问题...
相似文章
正确设计编程语言(2024)
这篇博客文章提出了新编程语言的设计指南,强调了修复常见问题,如缩进多行字符串、正确的文件路径处理、一致的文件扩展名和可扩展的语法特性。
金钉子:复活 Vale(n) 编程语言
这篇文章讨论了一个雄心勃勃的项目,旨在复兴Vale编程语言并创建一种名为Valen的新语言,目标是实现与Rust的无缝集成,以支持跨语言泛型和内存安全。
VLMs 在基准测试中能取得高分,但同时会静默地抹去有意义的术语并引入幻觉偏差 [P]
本文强调,用于胸部X光报告生成的 VLMs 在基准测试中能取得高分,但同时会抹去有临床意义的术语并引入有偏见的语言,并提出一个框架来度量这些失败。
迈向可理解的软件
本文批判了当前的编程实践和对大语言模型的依赖,反而主张通过更好的抽象、文档和软件栈来使代码更易于理解和维护。
揭示VLM可解释的故障模式
本文介绍了Revelio,这是一个通过搜索离散概念组合来系统性地发现视觉语言模型(VLM)中可解释故障模式的框架。应用于自动驾驶和室内机器人领域,它揭示了此前未报道的、可能导致碰撞或安全危险的漏洞。