编辑回复: 哈哈是的,大家要多多在留言区提问、回答、互动,学习气氛才会越来越好喔
作者回复: 追踪式 GC 的 STW 问题是很难避免的。所以 golang 也有同样的问题。Golang 在 2018 年之后性能好很多,golang 团队花了大力气优化,STW 从一个 GC cycle 的 10ms(2014 年)到两次 500us(2018年)。可以看这篇博客:https://go.dev/blog/ismmkeynote。
作者回复: Python 和 JS 的 primitive type 放在栈上,对象放在堆上,通过引用传递。
作者回复: String 在 Rust 中是一个智能指针,我们后续会讲到,它内部是一个结构体,放在栈上,结构体中有指针指向堆内存。所以 &s 指向一个栈上的地址。 多讲两句。{:p} 是通过 Pointer trait 实现。你可以看它的文档:https://doc.rust-lang.org/std/fmt/trait.Pointer.html。 下面的代码可以帮助你更好地理解数据在内存的什么位置: ```rust static MAX: u32 = 0; fn foo() {} fn main() { let hello = "hello world".to_string(); let data = Box::new(1); // string literals 指向 RODATA 地址 println!("RODATA: {:p}", "hello world!"); // static 变量在 DATA section println!("DATA (static var): {:p}", &MAX); // function 在 TEXT println!("TEXT (function): {:p}", foo as *const ()); // String 结构体分配在栈上,所以其引用指向一个栈地址 println!("STACK (&hello): {:p}", &hello); // 需要通过解引用获取其堆上数据,然后取其引用 println!("HEAP (&*hello): {:p}", &*hello); // Box 实现了 Pointer trait 无需额外解引用 println!("HEAP (box impl Pointer) {:p} {:p}", data, &*data); } ``` 你可以直接在 playground 里运行这段代码: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=5ca2cfb1d03936eae4b9a77a40d9987b
作者回复: 嗯,@pedro 回答得很详尽了。我补充一下。1. 在多线程场景下,每个线程的生命周期是不固定的,无法在编译期知道谁先结束谁后结束,所以你不能把属于某个线程 A 调用栈上的内存共享给线程 B,因为 A 可能先于 B 结束。这时候,只能使用堆内存。这里有个例外,如果结束的顺序是确定的,那么可以共享,比如 scoped thread;2. 而同一个调用栈下,main() 调用 hello(),再调用 world(),编译器很清楚,world() 会先结束,之后是 hello(),最后是 main()。所以在 world() 下用指针引用 hello() 或者 main() 内部的变量没有问题,这个指针必然先于它指向的值结束。这个两个问题的实质是我们要搞明白哪些东西在编译期可以确定它们的关系或者因果,哪些只能在运行期确定。
作者回复: 嗯,这是个好问题。这句话我表述地太绝对了。应该修改为:「在编译时,一切无法确定大小或者大小可以改变的数据,都无法**安全地**放在栈上,**最好**放在堆上」。 可变参数的函数是一个很好的例子。对于 Java,可变参数 String... a 是 String[] 的语法糖,它是放在堆上的。在 C 语言里,这个行为是未定义的,它只是定义了你可以通过 var_start / var_end 来获得可变参数的起始位置,以及最终结束可变参数的访问。但 gcc 的实现将可变参数放在栈上(估计是为了性能)。比如 C,你可以用 var_start / var_end 获取可变参数,但如果不小心处理, 会导致访问栈上的垃圾内容,甚至导致程序崩溃: ```C #include <stdio.h> #include <stdarg.h> int sum(int count, ...) { va_list ap; int i; double sum = 0; va_start(ap, count); for (i = 0; i < count; i++) { sum += va_arg(ap, int); } va_end(ap); return sum; } int main(int argc, char const *argv[]) { printf("%d\n", sum(10, 1, 2, 3)); // 传入 3 个值但 count 为 10 return 0; } ``` 同时谢谢 c4f 的提醒,alloca() 可以在栈上分配动态大小的内存,然而使用它需要非常小心,按 linux 的文档([https://man7.org/linux/man-pages/man3/alloca.3.html](https://man7.org/linux/man-pages/man3/alloca.3.html)),官方建议配合 longjmp 使用。alloca() 如果分配太大的数据,超过栈容量会导致程序崩溃,即使你分配很小的数据,但如果使用 alloca() 的函数被优化导致 inline,又恰巧出现在大的 for/while 循环中,也可能会导致崩溃。 所以,这两种在栈上分配可变大小的数据,是不安全的。
作者回复: 👍
作者回复: 多谢支持:)
作者回复: 是一份。它们指向 .rodata 中同样的地址。你可以用这段代码测试一下: ```rust fn main() { let x = "hello world"; println!("x = {:p}", x); foo(); } fn foo() { let v = "hello world"; println!("v = {:p}", v); } ```
作者回复: 主线程创建子线程并等待子线程结束这个动作,是我们通过程序的上下文得出的结论,但这是运行时的编译器并不能理解他们的因果关系。比如: ```rust fn main() { let x = 10; let handle = thread::spawn(|| { println!("x: {}", x); }); handle.join().unwrap(); } ``` 从代码中我们很容易得出结论,主线程的 x 被子线程引用是安全的,但在编译期编译器无法做出这样的保证。