关于编程:编程中的惰性思想

44次阅读

共计 2879 个字符,预计需要花费 8 分钟才能阅读完成。

前几天去东莞的路上看电子书《深刻了解 Linux 内核》,外面讲到 Linux 虚拟内存动态分配技术,联想到学习工作终波及到的一些经验,特写此文。(因我程度无限,不免有谬误之处,欢送大佬斧正~)

1. 惰性思维

我想大家在学习工作中,肯定或多或少有 Deadline 的经验吧,假期作业留到最初快开学了才写,衣服拖到最初没衣服换了才洗,这样做好不好呢?其实还行,个别 Deadline 期间写作业洗衣服效率挺高的,而换一个思路来说,在 写作业洗衣服 之前,你都是闲暇的,能够纵情做别的事件的,程序设计外面,我了解的 惰性思维 其实跟 Deadline 很像,资源分配推延到不能再推延为止

2. Linux 内存动态分配里的惰性思维

2.1 申请调页

术语 ” 申请调页 ” 指的是把页框 (虚拟内存调度单位) 的调配推延到不能再推延为止,也就是说,始终推延到过程要拜访的页不在 RAM 中时为止,因而引起一个缺页异样。

申请调页技术背地的动机是:过程开始运行时并不拜访其地址空间中的全副地址;事实上,有一部分地址兴许永远不会被过程应用。此外,程序的局部性原理保障了在程序执行的每个阶段,真正援用的过程页只有一小部分,因而长期用不着的页所在的页框能够由其余过程来应用。这样减少了零碎中的闲暇页框的平均数,从而更好的利用闲暇内存。从另一个观点来看,在 RAM 总数放弃不变的状况下,申请调页从总体上使零碎有更大的吞吐量。

2.2 写时复制(也就是赫赫有名的Copy On Write)

第一代 Unix 零碎实现了一种傻瓜式的过程创立:当 fork() 子过程时候,内核原样复制父过程的整个地址空间并把复制的那一份调配给子过程。而子过程有很多货色和父过程一样且不产生批改的,没必要齐全复制,这种做法既节约很多内存空间,又耗费许多 CPU 周期。

当初的 Unix 内核 (包含Linux) 采纳一种更为无效的办法,称为写时复制,也就是赫赫有名的Copy On Write。这种思维相当简略:父过程和子过程共享页框,只是不能批改,当批改的时候,就产生异样,这时内核再把这个页复制到一个新的页框中并标记为可写,原来的页框依然是写爱护的。当其余过程试图写入时,内核查看写过程是否是这个页框的惟一属主,如果是,就把这个页框标记为对这个过程可写。

其中,页描述符的 _count 字段用于跟踪共享当前页框的过程数目,为 1 时阐明跟其余 1 个过程共享,为 0 时阐明只有一个过程可写,为 -1 时阐明须要被开释。

3. 举几门语言的例子

3.1 JS语言反面教材

掘金上的前端开发同学比拟多,必然很理解 JS 的深浅拷贝问题,问烂了的面试题了

let a = {a:1};
let b = a;
let c = Object.assign({}, a)
console.log(a,b,c,a===b,a===c);
//{a:1} {a:1} {a:1} true false
a.a = 2;
console.log(a,b,c,a===b,a===c);
//{a:2} {a:2} {a:1} true false

下面的代码很好了解,b是浅拷贝,故而值跟着变了,c是深拷贝,故而值没变
而对应的变量内存地址的话,因为 JS 并不能像其余语言一样,显式的查看变量内存地址,但能够借助 ChromeMemory工具快照,来查看其相应的 V8 虚拟地址。

显著的,ab 有着雷同的内存地址,而 c 因为是深拷贝,领有新的内存地址;当咱们给 a.a = 2 从新写入后,察看 Snapshot 12,ab的内存地址仍然雷同,这是因为 JS 这门语言并没有实现写时复制。

3.2 Python写时复制

讲完 JS 反面教材,咱们再来察看一个实现了写时复制的语言。

terence@k8s-master:/mydata$ python3
Python 3.6.9 (default, Oct  8 2020, 12:12:24) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = "qwer"
>>> print(a)
qwer
>>> id(a)
140662197387536
>>> b = a;
>>> print(b)
qwer
>>> id(b)
140662197387536// 跟 a 的内存地址统一
>>> b = "qwert"
>>> id(b)
140662197387592// 写时复制,内存地址产生了变动
>>> 

3.3 PHP写时复制

PHP 对数组实现了写时复制,一般变量是没有写时复制的,通过上面的例子可能分明的看到内存占用量的变动,只是批改了 $b[0],而后整个都被复制了。(当然也能够装置xdebug 拓展,进一步能够察看到数组 refcount 援用计数的变动)

$a = [];
for ($i=0; $i < 10000; $i++) {$a[] = rand(0,10000);
}
var_dump(memory_get_usage());//9438664
$b = $a;
var_dump(memory_get_usage());//9438664
$b[0] = 99999;
var_dump(memory_get_usage());//9967104

3.4 Rust写时复制

fn main() {let s1 = String::from("hello");

    println!("s1 heap address: {:p}", s1.as_ptr());//0x5645e759d9e0
    println!("s1 stack address: {:p}", &s1);//0x7fff6a964bb8

    let mut s2 = s1;

    println!("s2 heap address: {:p}", s2.as_ptr());//0x5645e759d9e0
    println!("s2 stack address: {:p}", &s2);//0x7fff6a964c70

    b = String::from("world");

    println!("s2 heap address: {:p}", s2.as_ptr());//0x5645e759da70
    println!("s2 stack address: {:p}", &s2);//0x7fff6a964c70
}

对于 Rust 这种零碎级语言,咱们能更直观的察看到过程栈中的局部变量是如何跟堆中的理论内存地址关联起来的。

以理解 String 的底层会产生什么。String 由三局部组成,如下图左侧所示:一个指向寄存字符串内容内存的指针,一个长度,和一个容量。这一组数据存储在栈上。右侧则是堆上寄存内容的内存局部。

长度示意 String 的内容以后应用了多少字节的内存。容量是 String 从操作系统总共获取了多少字节的内存。长度与容量的区别是很重要的,不过在以后上下文中并不重要,所以当初能够疏忽容量。

当咱们将 s1 赋值给 s2String 的数据被复制了,这意味着咱们从栈上拷贝了它的指针、长度和容量。咱们并没有复制指针指向的堆上数据。换句话说,内存中数据的体现如下图

当咱们批改 s2 时,写时复制,变成了如下所示

4 其余例子

前端开发场景里有懒加载,节流防抖,其实都是惰性思维的体现,不到万不得已,不去分配资源。

正文完
 0