目录
0.引言与视频连接
1.多进程和多线程实现并发的编程的优势和劣势是什么?
2.协程为什么能实现更高的并发?
3.下面哪种访问数组的方式更快?
4.斐波那契数列(Fibonacci sequence)
5.哈希表和二叉查找树各自的优缺点
6. 解决hash冲突的方法及各自优缺点
7.谈谈自旋锁
8.读写锁用于解决什么问题-读优先和写优先是什么意思?
9. 怎样一个磁盘上的文件快速发给客户端?
10.相比堆,为什么栈上分配对象的速度更快?
0.引言与视频连接
https://www.bilibili.com/video/BV1nQ4y1T7FA
陶辉老师简历:
北京广东信达有限公司--2004年网管
华为--网管
腾讯--2007年--面试官猫眼CEO郑志昊Peter
思科--wedfx--但是全球最大的一个视频会议系统
阿里云
创业
https://baike.baidu.com/item/%E9%83%91%E5%BF%97%E6%98%8A/7257297?fr=aladdin--郑志昊百度百科
性能面试中很重要,在晋升中也很重要.
性能需要沉下心学习,如果对性能有深耕,写出的代码会质量很高.
1.多进程和多线程实现并发的编程的优势和劣势是什么?
切换的代价是不一样的,涉及到地址空间的问题,多进程挂了不会影响到其他进程,每个进程都有
独立的进程空间,所以Nginx使用多进程,它是特别强调稳定性.
老师说的evoal?是啥--多线程的.
一个线程一旦挂了,就会把整个进程中的所有线程的地址空间都写乱,就全部挂了.
多进程的劣势:
因为是独立的地址空间,所以通信起来就相对困难.线程通过共享内存就可以实现通信了.
我的想法:
其实俺觉得,通信困难也是相对的,毕竟人家多进程有那么多种通信方式.
2.协程为什么能实现更高的并发?
1.切换速度非常快,进程和线程如果要切换需要在内核态进行切换,但是协程是在用户态
进行切换,所以不用进入内核态的话,上下文切换的成本就没有了;
2.和异步编程有关,因为我们在网络编程中使用多线程的时候往往使用同步的阻塞的socket,
磁盘IO的编程也有同样的道理,如果调用一个阻塞的API,如果没有完成任务就让线程休眠,使用协程
则一般不会用阻塞的API,协程默认会创造一套无阻塞的生态,这个生态中所有的API和SDK都是
没有阻塞的,可以让协程自己在用户态进行调换;
3.实现高并发的瓶颈是什么呢?比如说一个百万连接吧.每一个连接都是要占用内存的,比如说在linux
下线程有对应内存池的概念,默认情况下开一个线程会分配64M内存(非常恐怖),所以很多时候Java程序
员在JVM启动之后会多出几个G的内存,这个其实是线程的内存池造成的,所以我们想100w并发请求,线程
是绝对做不到的,除了线程的area(堆内存),还有响应的栈部分的内存,每一个线程都会有自己的栈(大
概是8M,这个是可以设置的),这个内存要求是不可能支撑百万连接的.协程的优势就有了,一个协程所占
内存只有十几K或者几十K,所以可以轻松实现百万甚至千万并发.
3.下面哪种访问数组的方式更快?
方法1
for(i=0;i++;i<n)
for(j=0;j++;j<n)
array[i][j]
方法2
for(i=0;i++;i<n)
for(j=0;j++;j<n)
array[j][i]
本题考查的是CPU缓存,本题有三点:
(1)内存布局,二维数组的大部分编程语言都是按行布局的,一行数据放完了再放第二行,以此类推,
基于这个概念,我们再来看CPU的缓存;
(2)CPU中的缓存;
(3)CPU中的缓存行的大小.
如果想要把系统的性能优化到极致,您必须清楚地了解主机相关的几大件(CPU,内存,磁盘,网络等)的
速度到底有多快.一台好的电脑可能会有3级CPU缓存,普通的可能有2级CPU缓存.一般来说访问速度大概
如下:
寄存器 1nm
一级CPU缓存 3-5nm
二级CPU缓存 10-20nm
三级CPU缓存 20-30nm
内存 100nm
为什么要有缓存呢?因为可以提前把需要访问的数据批量地载入到缓存,缓存的数据非常快,CPU就不用
总是在等待,因为每次计算一个数据都需要把程序的指令读到CPU运算,想要写回去也会很慢,有了缓存,
缓存和内存之间的通讯就相对独立开了,CPU就可以全力工作起来了,所以我们此时必须清楚,访问
array[i][j]的时候是批量载入缓存中的,array[i][j+1],array[i][j+2],array[i][j+3]等都被载入
到缓存中去了,所以arr是一个数字的情况下,一次性会载入一个CPU_Cache_Line(目前是64字节)个字
节,如果每个元素是8字节的,一次性可读入8个数组元素,此时array[i][j]可以利用这个特性,但是
array[j][i]就无法利用这个特性了,因为它是按列进行布局的.
这道题的思想和我们息息相关,优秀的开源项目中的代码中都会充分利用这个特性.功能相同的代码,
为什么有的快有的慢,有的甚至相差很多,也许就是这些细小处的原因.
C++语言下的遍历arr[i][j]可以比arr[j][i]快7倍多,如果java可能也会快一些,python中可能也有
区别,具体要看封装细节和测试结果.
从这个细节,我们可以发现好的开源代码作者非常关注CPU缓存,内存池中的问题,这些都体现了功力
的问题,但愿我们都能成十倍程序员.
--局部性原理
斐波那契数列(Fibonacci sequence)
CPU的Cache与缓存行
Linux上查看缓存行的大小
4.斐波那契数列(Fibonacci sequence)
谷歌一道非常有意思的面试题,考查以下点:
1.递归概念;
2.时间复杂度,我们在谈性能的时候,一定要从算法复杂度这个概念去思考的.能不能从递归函数中推导
出2的n次方,这个在现实中非常复杂的,几乎不能用;
3.能不能优化,将时间复杂符从2的n次方降低到n,换一种方式,可以自顶向上计算,计算完1计算2...
倒着推,一个for循环就可以解决.
4.用公式法,展现您的数学功底,震惊你的面试官吧,实现O(1)复杂度.
5.哈希表和二叉查找树各自的优缺点
基本概念:
二叉树:每个结点最多有两个小孩;
二叉查找树:是一个有序的二叉树;
平衡的二叉查找树:如红黑树.
回答本题需要注意:
(1)CRUD的各自时间复杂度,红黑树logn,哈希表好的情况下是O(1),比较好的情况下,哈希表是快于红黑树的;
(2)要说明缺点:
哈希表:
a.哈希表的遍历的性能不能满足我们的需求,特别当我们希望哈希表的冲突比较少的时候,在设计及的
后序实现的时候,会产生很多空槽(空的bucket),也就是说此时的装载因子是比较小的,这时遍历会
做很多无用功,可以遍历但效果会比较差;
b.做范围查询查不了,因为是基于hash函数,映射到一个位置,但是查找另一个key对应的值的时候,
可能key是相邻的,但是value完全没有什么关系,就没有办法做范围查询,但是范围查询又是我们
很多时候会经常用到的,所以这个是哈希表的问题.
二叉查找树:
a.平衡的问题;
b.当元素很多的时候,树高称为一个问题,针对树高太高的问题此时我们就可以考虑其他类型的树,比如
说我们现在做一个索引支持范围查询的时候就使用了B+树,B+树的底层数据会放到磁盘中.
6. 解决hash冲突的方法及各自优缺点
不同的key可能会映射到同样一个位置上,有时候我们的key非常大,比如说是一个字符串,但是
位置是很小的,因为hash表是有限的,位子其实就是在数组中的一个位置,这个数组可能非常小,
但是前面的信息量是非常大的,一般来说都要进行压缩的,这样下来冲突就不可避免了.
哈希冲突的两种方法:
(1)开散列:在hash数组之外去存放有冲突的元素,也叫作链表法,当多个元素映射到同一个元素的时候
就变成一个链表了,此时的查询时复杂度会退化成O(n);
(2)闭散列:冲突之后,换一个hash函数,会产生一组hash函数,这一组hash函数可以与我们的冲突次数相
关.比如说有一个新的key存入的时候发现冲突了,就换第二个哈希函数,其中第二个hash函
数,第二个哈希函数中可以引入一个冲突次数,整个函数中将冲突次数作为一个自变量引入,
最后得到的结果就变了,所以位置就自然地变了,这个就是闭散列.
(1)开散列:哈希函数设计相对简单;
在容灾场景中特别适合用作持久化;
(2)闭散列:对哈希函数的设计要求复杂一些.
在腾讯的工作中,陶辉老师曾经设计过一个hash表,这个hash表需要跨服务器对数据进行存放,需要容灾,
当出现问题的时候需要从另一台服务器将数据再弄过来,这时候如果使用开散列就非常糟糕了,因为首先
为了速度这个哈希表是放在内存中的,其次为了传到另一台服务器肯定是要序列化的(如果通过其他的方
法来做序列化是无法控制的),最快的方法就是利用映射,把内存映射到磁盘中,此时如果有链表,想要映
射就非常困难了,需要很多逻辑并做很多事情.但此时如果是一个闭散列,就会特别简单,把整个数组映射
下来作为一个文件传到另外一台机器上就可以了.
所以闭散列在持久化上的优势非常大,一般来说,在分布式的容灾上,闭散列是非常香的.
场景思考:
一个数据库一个用户表已经存不下了,再放性能就会变很差,需要通过hash函数将其挪一下位子,取模,
比如说有10张表就模10,32表就模32,就可以换到不同的表上去.
一个好的哈希函数:
(1)减少冲突,扩容,当内存不值钱的时候,可以将装载因子变小,100个槽位只用10个,就能很好散列;
(2)运算速度快,需要充分考虑位运算和数学上的知识.
(3)动态扩容.
哈希表的扩容是一个比较麻烦的事情,最简单的是停下来重新映射,映射到一个扩容后的哈希表,但这时哈希表的可用性为0了,这个不是最优解.所以我们提出“动态扩容”,一边迁移一边为客户提供服务,
如果访问到某个值,可以稍微慢一点,但是迁移要持续下去.
动态扩容涉及到单机内和跨服务器的,但道理是一样的,关键是要有一个标签能给出这个值现在是在老
哈希表中还是新哈希表中,用异步的一个线程逐步扩容,每次查询的时候先判断在新表还是旧表中即可.
因为数据一般是有规律的.腾讯QQ号分库分表的时候,最初是模100,这个会把
最后具有很低辨识度的两位数字的影响放大,对靓号的追求会使得有些人们眼中的好号码的热点很高,
有些数字遭受冷落,QQ号最后两位数便是靓号的一个重点,所以模100是个很糟糕的方法.
之后改成99,其实也不是最好的.从数学上来说,mod一个较大的素数,比如说101等素数对解决冲突来说
非常好.
模32是个非常糟糕的方法,一个懂得性能优化的程序员,写代码的时候写mod32和右移5位相比,右移5位
的效率要高很多;
java的标准库中对字符串求hash的时候就用到了一个质数31,每个字符的ASCII码乘以31,左移五位再
减去n和乘以31的数值是一模一样的;--这一点没听明白..
手机号做主键,前三位(运营商)是个很糟糕的选择,中间四位(区域)也是一个糟糕的选择,后四位相对
来说就是一个比较好的选择,身份证也需要做同样的考虑.
7.谈谈自旋锁
自旋锁--世界上最不撞南墙不回头的锁.
自旋锁和互斥锁场景比较:
自旋锁:比如有个洗手间里面有个人,我很着急,我就一直敲门敲敲敲敲敲催ta出来;
互斥锁:比如有个洗手间里面有个人,我不是很着急,我敲了一下门,没出来,我就不敲了,等ta出来,我就
有机会进去,但是要注意ta出来了以后能进去的不一定是你噢.
有啥差异:忙等待和休眠等待;
自旋锁:一直在等;
互斥锁:等的时候可以看看电影啥的;
锁的选择涉及到应用场景:
如果你是要做一个在几步内就能完成的操作,比如说对一个整型变量赋值,对一个字符串赋值,简单地计
算,这种情况下使用自旋锁是最合适的.因为此时基本没有什么性能损耗,不会发生线程切换等行为,而互
斥锁会导致调度任务(线程)发生切换,这个是有代价的,linux下差不多有几毫秒.
本题考查:
(1)自旋锁的实现,表面上是忙等待,但是绝对不能写个while(1)让ta空转,会导致CPU的使用100%,向intel
之类的提供商会提供pause这样的指令(可以省电),不至于导致CPU一直在转耗电太大;
(2)判断一块代码的执行时间,在实际应用中判断这块代码到底适不适合使用自选锁.
8.读写锁用于解决什么问题-读优先和写优先是什么意思?
如果能够区分对资源要做的操作是读或者是写,就可以用读写锁.
读锁共享资源,写锁独占资源.
使用场景:读多写少.
读写锁的实现方式:大部分使用互斥锁实现,也可以用自旋锁实现.
读优先:
1号线程在读,2号写线程来了等着,3号读线程来了可以共享一号的读锁,4号读线程来了可以共享一号的
读锁...会造成2号写线程饿死(写线程饿死的前提是一直有读线程占着锁,一旦出现锁释放没被占用,就会
被写线程拿住);
写优先:
1号线程在写,2号读线程来了等着,3号写线程来了一旦一号的写锁释放立刻拿到,4号写线程优先级也比
读线程优先级高...会造成2号读线程饿死;
JAVA中为了防止线程被饿死,设计出retrainreadwritelock这样的锁,公平读写锁,会把没拿到锁的线程
安排进一个队列中,会有优先级,效率稍低,但不至于有线程被饿死.
9. 怎样一个磁盘上的文件快速发给客户端?
传统的方法:
大小不知道,一般不会分配较大块内存,如果一个文件1G,要在内存中分配1G,100M的压力
可能都会很大,可能直接就OOM了.所以可能在用户空间定义一个比如说32K的空间每次读32K,
再往网络上发送.
传统的方法问题:
(1)内存拷贝次数太多:磁盘文件会首先拷贝到操作系统的一个高速缓冲区,
再从高速缓冲区拷贝到用户态分配的那32K,
再把32K拷贝到内核分配的socket缓冲区,
再把socket缓冲区的内容拷贝到网卡上.
4次拷贝.
(2)切换次数多:调用一次read,会发生两次切换(从用户态切换到内核态,再从内核态切换到用户态);
调用send也是两次切换.
解决方案:调用sendfile.
零拷贝的优点:
(1)两次系统(read和send)调用变成一次(sendfile),拷贝也少了;
(2)充分使用了tcp的缓冲区,一般来说tcp的缓冲区是1M多,tcp的缓冲区是动态变化的,用户是
不清楚的,32K很小,但用户也不敢去分配1M去充分使用内核的tcp缓冲区,但是内核是明明
白白的,内核自己来发送的时候会用最大化它发送的长度,比如说每次拷贝1.4M,这样大量
减少切换次数.这样就会快很多.用了0拷贝以后,性能可能能增加1倍.(动手测一下).
man sendfile
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
10.相比堆,为什么栈上分配对象的速度更快?
原因:
(1)每个线程都有一个独立的8M(可设置)的栈空间,这块空间是线程独有的,底层库不需要对这块
资源加锁,为线程独有,没有锁就会快很多,大概快了多少,需要测试,老师有个课程?之后看看.
(2)已经预分配好了,每次只要移动一下就好了,但是堆上的内存需要自己去申请再分配.
简单来说:
(1)底层未加锁;
(2)预分配内存.
拓展:
栈的问题:
(1)生命周期有限;
(2)资源受限,深层次的递归调用很容易导致栈溢出.