-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy path04.srt
3899 lines (2929 loc) · 103 KB
/
04.srt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1
00:00:00,542 --> 00:00:07,999
我们现在开始上课,嗯,首先今天的主题是编译器优化
2
00:00:07,999 --> 00:00:10,999
然后编译器是什么呢?
3
00:00:10,999 --> 00:00:16,684
众所周知,编译器就是从源代码生成汇编语言,
4
00:00:16,708 --> 00:00:27,999
所以这次要从汇编语言的角度来理解编译器是怎么优化的,以及我们如何用好它
5
00:00:27,999 --> 00:00:34,999
首先这是一个系列课,我们今天已经是第四讲了
6
00:00:34,999 --> 00:00:44,999
然后这次我们会基于GCC,然后还有64位x86架构,来具体讲讲
7
00:00:44,999 --> 00:00:55,724
然后第0章首先要讲解一下什么是汇编语言,就是首先是x86的64位架构,
8
00:00:55,748 --> 00:01:02,792
还有这些寄存器。寄存器呢,它又是在CPU里面的
9
00:01:02,792 --> 00:01:10,999
然后它的速度比内存快,如果读到寄存器里,然后再计算就比较高效
10
00:01:10,999 --> 00:01:19,999
所以x64它提供了这么多寄存器,其中白色的这些是32位模式下就有的
11
00:01:19,999 --> 00:01:27,487
而64位以后,不仅把原来32位的这几个扩充到64位,
12
00:01:27,511 --> 00:01:34,999
它还额外追加了8个寄存器,这样我们用起来就更方便了
13
00:01:34,999 --> 00:01:44,999
然后RIP是它的当前执行的这个代码的地址,嗯,也是扩充到64位
14
00:01:44,999 --> 00:01:56,999
然后还有这些MMX,还有YMM,XMM这些,这些都是那个用于存储浮点数的寄存器
15
00:01:56,999 --> 00:02:03,754
它们能够,就是一个,就是这个不是一个有128位宽嘛,
16
00:02:03,778 --> 00:02:15,999
然后float 它不是有32位宽嘛,所以这里面可以塞得下四个float
或者塞两个double
17
00:02:15,999 --> 00:02:24,999
然后他们在运算加法的时候就可以两个double一起来算这样它效率就更高了,为什么
18
00:02:24,999 --> 00:02:28,209
为什么说浮点数用的是这个呢?
19
00:02:28,209 --> 00:02:35,198
因为浮点数就是我们高性能计算中经常用到浮点数,
20
00:02:35,222 --> 00:02:43,125
所以就干脆把他们认为是那个,嗯你懂的,就是比较高效嘛
21
00:02:43,999 --> 00:02:59,999
明白吗?哦,对我得开一下弹幕,嗯,然后我这个就关掉了
22
00:03:02,292 --> 00:03:12,999
然后就是刚才说的32位,只有这八个寄存器,然后到64位以后又新增了八个寄存器
23
00:03:12,999 --> 00:03:23,999
可以看到这几个没有在命名强迫症了,它们直接用数字来编号,然后寄存器多有什么好处呢
24
00:03:23,999 --> 00:03:34,999
就是比如,你的局部变量有16个,那它们就都能够存进寄存器里,而不需要存到内存上
25
00:03:34,999 --> 00:03:43,999
这样编译器它就可以自由的把你的变量存到这个局部的这个
26
00:03:43,999 --> 00:03:50,999
就是说把你的局部变量变成一个寄存器,这样它读写就更快了
27
00:03:50,999 --> 00:03:56,999
所以说64位除了内存更大,它还有性能的优势
28
00:03:57,999 --> 00:04:08,999
然后就是说有刚刚不是看到就是32位叫eax,到64位变成rax,它们是什么关系呢?
29
00:04:08,999 --> 00:04:19,999
它们是共用前32位的关系,就是eax这个地方的值和rax的低32位是共用的
30
00:04:19,999 --> 00:04:30,709
同理还有16位的rax它和eax共用的低16位,然后ah是ax的高八位,然后al是低8位
31
00:04:30,709 --> 00:04:41,999
当然r开头的这些数字编号寄存器也有,他们是通过bwd然后没有来区别的,有问题吗?
32
00:04:42,999 --> 00:04:57,999
对,AVX它用的是zmm,AVX512 它是zmm,然后AVX用的呢是ymm,它是256位
33
00:04:57,999 --> 00:05:07,999
然后还有最普通的SSE,用的是xmm,它们只有那个128位
34
00:05:08,024 --> 00:05:25,858
然后就是我们说就是汇编语言大致分为两种,一种是英特尔模式的汇编,它写寄存器就直接写eax,
35
00:05:25,882 --> 00:05:35,024
然后它写操作数,就比如eax赋值给edx,就把edx写在前面,eax写后面
36
00:05:35,999 --> 00:05:42,999
然后用mov指令就把eax赋给edx,而AT&T汇编它
37
00:05:42,999 --> 00:05:53,500
恰恰相反,它读的那个数字写在前面的,写的那个数字写在后面的,也就是说这个是往右移动的
38
00:05:53,500 --> 00:06:00,292
而英特尔是往左移动的,包括它的立即数,也是用一个S符号
39
00:06:00,292 --> 00:06:10,999
然后它也是往右移的,嗯,然后jmp指令它还要以额外的乘法符号以及它的操作数长度,
40
00:06:10,999 --> 00:06:12,980
就是英特尔就直接mov,
41
00:06:13,004 --> 00:06:28,792
而我们这个AT&T它后面要加一个movl,代表是long
,也就是32位,而b呢就是八位,w呢就是16位
42
00:06:29,999 --> 00:06:33,999
然后他的这个访问地址呢也不一样
43
00:06:33,999 --> 00:06:42,458
这里英特尔是更直观的一个[],然后这里写偏移量,而它是把偏移量写在()前面,
44
00:06:42,482 --> 00:06:50,417
然后后面来一个(%dx)代表它偏移的寄存器,明白吗?
45
00:06:52,999 --> 00:07:03,999
然后它访问全局变量也不需要这个[],直接以全局它的地址,就用这个立即数的符号($)
46
00:07:06,999 --> 00:07:16,999
然后就是这些,总之非常复杂,但是GCC用的就是这一种,没办法,我只好用它
47
00:07:16,999 --> 00:07:26,999
然后就是说就,不是有一种函数嘛,然后这个ret是函数的返回指令,然后这个是赋值指令
48
00:07:26,999 --> 00:07:30,999
所以可以看到这个func() return 42
49
00:07:30,999 --> 00:07:36,685
它被编译成了把42复制给EAX然后返回,
50
00:07:36,709 --> 00:07:45,999
就可以看出我们这个返回值啊是通过EAX传出去的,明白吗?
51
00:07:47,999 --> 00:07:56,999
就是我这里是用这个指令来编译一个cpp文件,变成一个.s文件
52
00:07:56,999 --> 00:08:05,999
然后-s就代表生成的不是.o文件哦,而是而是这个.s的汇编语言文件
53
00:08:05,999 --> 00:08:11,999
然后这个呢,就是可以让生成代码更简洁
54
00:08:11,999 --> 00:08:13,991
然后这个呢,只是嗯,
55
00:08:14,015 --> 00:08:25,999
就是说让它的每一条汇编前面有一个注释,来提示这一条指令代表的是原文件当中的第几行
56
00:08:25,999 --> 00:08:34,334
比如return 42就代表了movl $42 %eax这一行,这就能够便于我们学习
57
00:08:36,292 --> 00:08:44,999
对的,它会自动设置的,不需要你去指定,明白吗?
58
00:08:49,999 --> 00:08:54,814
然后就是这个解释,
59
00:08:54,838 --> 00:09:03,424
然后可以看到我们这里有一个有很多很多参数的那个函数,
60
00:09:03,448 --> 00:09:11,999
然后它有123456个参数,它们是通过寄存器,这些寄存器传入了
61
00:09:12,999 --> 00:09:23,999
然后可以看到它这里先是把所有传入的寄存器给先存到一个堆栈上面
62
00:09:23,999 --> 00:09:30,999
rsp就代表堆栈,而这个-就代表是堆栈上的某一个地址
63
00:09:35,999 --> 00:09:39,999
然后它的返回值不是返回了a吗?
64
00:09:39,999 --> 00:09:43,999
就是说edi它不是存到了a变量吗?
65
00:09:43,999 --> 00:09:55,999
它现在又取出来设为这个返回值,然后调用它的人一看eax就知道它返回了多少,明白吗?
66
00:09:58,584 --> 00:10:05,931
然后就是这个访问地址这个边这个符号就是先弄一个立即数,
67
00:10:05,955 --> 00:10:13,999
就代表它的偏移量,然后是它的寄存器写在()里,是这个意思啊
68
00:10:15,584 --> 00:10:19,999
然后就是刚才不是用这个指令吗?
69
00:10:19,999 --> 00:10:28,999
现在我们可以通过加一个-O3选项,加了这个flag 之后,他就比刚才更优化
70
00:10:28,999 --> 00:10:34,999
就刚才不是还把b,c,d,e,f都传了一遍到栈里吗?
71
00:10:34,999 --> 00:10:41,605
现在直接简单了,直接把a传进去,就给送到eax,
72
00:10:41,629 --> 00:10:47,999
就它只需要一行指令就够了,这就是编译器的自动优化
73
00:10:47,999 --> 00:10:53,999
它发现这几个存到栈,你后来没有用过啊,就是只有这个是用到的
74
00:10:53,999 --> 00:10:57,999
所以它直接把这些指令全部砍掉了
75
00:10:57,999 --> 00:11:03,999
然后它又一想,哎,我这写入了一遍栈,又读了一遍栈,没意思啊
76
00:11:03,999 --> 00:11:10,999
我直接把edi复制给eax不就好了嘛,所以它就优化成了这样
77
00:11:11,417 --> 00:11:19,999
所以开启这个选项之后,它就能够生成更精炼更高效的代码
78
00:11:23,999 --> 00:11:25,999
我在录吗?
79
00:11:25,999 --> 00:11:27,999
应该在录
80
00:11:28,974 --> 00:11:31,082
然后这个就
81
00:11:31,106 --> 00:11:41,974
然后就是除了可以movl 来复制之外,这个汇编语言还支持这个乘法,还有加法之类的指令
82
00:11:41,999 --> 00:11:52,999
比如乘法就是imul后面又多了一个l和movl是一样的,代表它的操作数是32位的
83
00:11:52,999 --> 00:11:56,999
然后看一看这是做了什么事呢?
84
00:11:56,999 --> 00:12:05,178
edi我们知道是代表的a,第一个参数,然后esi就是第二个参数,代表b,
85
00:12:05,202 --> 00:12:08,999
然后这是为什么呢?
86
00:12:08,999 --> 00:12:16,625
看一下这个imull %esi, %eax就代表把eax 乘等于(×=)esi,
87
00:12:16,649 --> 00:12:24,999
也就是eax乘以esi以后,再把结果写回到eax, x86汇编
88
00:12:24,999 --> 00:12:36,751
就是这样,他都是就地乘,没有写入到另一个寄存器的这种,它是一种(没听懂)的
89
00:12:36,751 --> 00:12:53,418
所以就是,这里不是首先是把edi赋值给eax嘛,
90
00:12:53,442 --> 00:13:05,999
所以它就相当于,先就是把eax变成了a,然后再把b乘以到a里面,就变成这个返回值了
91
00:13:07,999 --> 00:13:17,999
然后就是64位的乘法,就是刚才不是说l代表32位嘛,而q就代表64位
92
00:13:18,085 --> 00:13:20,633
所以我们这个long long它是64位的,
93
00:13:20,657 --> 00:13:31,999
然后64位也从e系列寄存器变成r系列寄存器,就更宽了,能存储64位
94
00:13:36,999 --> 00:13:39,540
然后就是整数加,
95
00:13:39,564 --> 00:13:48,999
我们以为它会生成一个addl,也就是add没想到它却变成一个lea指令
96
00:13:48,999 --> 00:13:50,999
lea是什么呢?
97
00:13:50,999 --> 00:13:55,812
可以看到这里是一个地址的那个操作数,
98
00:13:55,836 --> 00:14:04,999
也就是这个地址其实是相当于相当于这个的*(rdi+rsi),就是这样的
99
00:14:04,999 --> 00:14:13,167
lea呢又是加载一个表达式的地址,而不是把这个表达式值给取出来
100
00:14:13,167 --> 00:14:21,538
他取的是这个地址,所以这个就相当于eax等于&*,也就是这两个会抵消,
101
00:14:21,562 --> 00:14:26,999
就相当于eax等于rdi加rsi
102
00:14:27,999 --> 00:14:40,167
所以说这是在妙用这个加载地址指令,把它当做add来操作,它就可以更简练
103
00:14:40,167 --> 00:14:43,999
明白吧?
104
00:14:45,999 --> 00:14:54,999
然后就是leal除了可以直接执行加法,它还可以执行任何一次函数
105
00:14:54,999 --> 00:15:01,999
比如这个表达式,因为它是这里多了一个逗号
106
00:15:01,999 --> 00:15:04,999
8就代表把第二个数乘以了8
107
00:15:05,999 --> 00:15:15,999
因为这种线性变换经常用于地址访问中,所以x86把它做成了这个地址访问的操作符里
108
00:15:15,999 --> 00:15:25,751
然而这个lea呢却能把这个地址不是去读它的值,而是去读它这个地址读出来
109
00:15:25,751 --> 00:15:33,542
所以说这就变成了,我们可以把任何一个一次函数给写在一条指令里
110
00:15:33,999 --> 00:15:41,999
所以说这种一次函数在x86上都是很高效的
111
00:15:43,999 --> 00:15:51,584
然后就是刚才说到这里是,为什么要线性变换做成指令呢?
112
00:15:51,584 --> 00:15:57,298
就是为了针对这种情况,比如func(int*a, int* b)然后a是一个指针,
113
00:15:57,322 --> 00:16:07,538
然后我要返回a[b]这时候你会想a这个是怎么知道a[b]的地址,然后读它呢,
114
00:16:07,562 --> 00:16:14,999
你可能会想:哦,那简单,不就是a加b吗?
115
00:16:14,999 --> 00:16:17,367
不行,因为a它是一个int类型的指针,
116
00:16:17,391 --> 00:16:28,459
而如果你直接在汇编里面去a加b的话,它会变成一个char类型的加,
117
00:16:28,459 --> 00:16:35,984
所以说你要char类型的加,加上这个sizeof(int)乘以b,就是像这样,
118
00:16:36,008 --> 00:16:46,999
就是因为int 的大小是4 ,所以这个偏移量也要乘以4才能访问到正确的地址的
119
00:16:46,999 --> 00:16:55,999
所以这里就用了这个x86提供的这个很方便的线性函数来访问
120
00:16:58,292 --> 00:17:01,999
那么这个语句又是啥意思呢?
121
00:17:01,999 --> 00:17:04,167
为什么movslq,
122
00:17:04,191 --> 00:17:13,999
哦,原来就是因为我们刚刚用的int 是32位的,而指针和地址都是64位的
123
00:17:13,999 --> 00:17:25,959
所以说要用这个这个64位的a去加上一个32位的b之前首先要把b转换成64位的
124
00:17:25,959 --> 00:17:34,999
可以看到我们这里是把esi这个32位变成rsi这个64位的了
125
00:17:34,999 --> 00:17:39,626
这样以后才能进行这个线性访问
126
00:17:39,626 --> 00:17:41,958
那么如果要避免呢,
127
00:17:41,982 --> 00:17:48,445
就可以用我们<cstdint>
里提供的std::size_t,
128
00:17:48,469 --> 00:17:56,667
这个size_t它能够保证在64位系统上就是int64
129
00:17:56,667 --> 00:18:02,999
而32位系统上相当于uint32 ,所以就不需要再进行
130
00:18:02,999 --> 00:18:09,125
刚才那个先从32扩展到64 ,它直接就是64位的
131
00:18:09,125 --> 00:18:14,805
所以说它有可能更高效,而且它还有一个好处,
132
00:18:14,829 --> 00:18:26,999
就是比如你这个数组的大小超过INT_MAX,也就是你的数组大到超过2^31次方了
133
00:18:26,999 --> 00:18:32,209
这时候int 就不仅是效率的问题,它是会出错的
134
00:18:32,209 --> 00:18:39,999
所以我们就要用64位的size_t来表示超过了那个大小的索引
135
00:18:40,584 --> 00:18:43,272
明白吧?
136
00:18:43,296 --> 00:18:57,999
所以说建议就是你如果再用于这个下标的话,最好用size_t包括用于循环体的那个int
i=0
137
00:18:57,999 --> 00:19:02,999
i<xxxx,这个也推荐用size_t
138
00:19:03,999 --> 00:19:11,999
然后就是刚才说到的浮点数,什么是浮点数呢?
139
00:19:11,999 --> 00:19:25,458
就比如这种123
,这个是那个这个是整数,然后这个呢就是浮点数,然后浮点数有一个著名的笑话,
140
00:19:25,482 --> 00:19:34,999
就是总之就是0.2加0.1不等于0.3 ,这是为什么呢?
141
00:19:34,999 --> 00:19:37,574
因为浮点数它有精度误差。
142
00:19:37,598 --> 00:19:52,498
不会手写汇编,应该不会吧,不会手写汇编,但是会手写一些编译器指令,比如MM开头的那些东西,行吧?
143
00:19:52,522 --> 00:19:57,999
然后就是说浮点数它不是完美,它是有误差的
144
00:19:57,999 --> 00:20:01,665
就所以说发明了定点数,
145
00:20:01,689 --> 00:20:06,684
就是2000加100 ,然后再除以1000 ,
146
00:20:06,708 --> 00:20:12,375
我们这样就能够保证它没有误差
147
00:20:12,375 --> 00:20:20,999
所以浮点数呢它的好处就是能够表示很大范围内的数,但是它有误差
148
00:20:20,999 --> 00:20:31,999
不过我们高性能编程中经常会用到浮点数,所以说CPU也对他们做了专门的指令
149
00:20:31,999 --> 00:20:42,999
比如add指令,就是刚才不是说addl是代表那个两个32位整数相加嘛
150
00:20:43,999 --> 00:20:49,999
然后这个s呢就代表single ,也就是单精度浮点数
151
00:20:49,999 --> 00:20:52,447
我们的float 就是单精度浮点,
152
00:20:52,471 --> 00:20:57,038
然后addss就代表这两个数相加,
153
00:20:57,062 --> 00:21:00,925
然后他传参数也不是edi,esi了,
154
00:21:00,949 --> 00:21:03,858
他直接用xmm系列,
155
00:21:03,882 --> 00:21:19,811
就是xmm0 就是传a, xmm1就是传b, 然后我们把xmm1加到xmm0
,然后因为正好xmm0是用于返回值的,所以我们这里直接返回了,就可以是a加b了,
156
00:21:19,835 --> 00:21:21,999
明白吗?
157
00:21:26,999 --> 00:21:32,931
参数是0和1 ,返回值是0 ,
158
00:21:32,955 --> 00:21:42,392
然后就是提刚才提到了xmm0这个系列的寄存器,它们都有128位宽的,
159
00:21:42,416 --> 00:21:47,999
它可以容纳四个float 或者两个double
160
00:21:47,999 --> 00:21:59,999
刚才的案例,因为只有一个float 存在一个大128位的计算器,所以只用到了它的最低32位
161
00:21:59,999 --> 00:22:01,999
但是这样有一个问题
162
00:22:01,999 --> 00:22:07,999
因为我们刚才说的是,addss它会只会加最低位
163
00:22:07,999 --> 00:22:12,959
这就要说到这里就是addss什么意思呢?
164
00:22:12,959 --> 00:22:14,999
它要分成三个部分
165
00:22:14,999 --> 00:22:23,375
首先是第一个s它代表的是标量,也就是说只对它最低32位去计算
166
00:22:23,375 --> 00:22:31,999
然后也可以是addps,这时候就会把xmm里所有的四个float 都进行运算
167
00:22:32,999 --> 00:22:40,578
然后就是第二个s它不一样,它代表的是单精度浮点,
168
00:22:40,602 --> 00:22:46,999
也就是float 类型,它也可以是d表示双精度浮点double 类型
169
00:22:47,667 --> 00:22:51,375
然后就所有的排列组合共这四种
170
00:22:53,792 --> 00:22:59,105
第一种是addss是一个float ,addsd一个double,
171
00:22:59,129 --> 00:23:07,999
addps因为xmm有128位可以存四个float,所以他会把四个都进行那个加法
172
00:23:07,999 --> 00:23:14,999
就像这样,就是比如这是xmm0 ,这是xmm1 ,这是xmm0
173
00:23:14,999 --> 00:23:21,999
然后我们把这个加这个存进去,这个加这个,就是同时计算四个的加
174
00:23:21,999 --> 00:23:28,425
但是如果是addss的话,它只会对最低位进行一个加法,
175
00:23:28,449 --> 00:23:35,000
然后其他的位都保留叉xmm0原来的值。明白了吧?
176
00:23:37,999 --> 00:23:40,647
然后这里是省流助手,
177
00:23:40,671 --> 00:23:45,998
就是如果编译器生成了很多大量ss结尾的指令,
178
00:23:46,022 --> 00:23:54,104
那就说明他只是在最低位进行计算,没有进行那个矢量化,
179
00:23:54,128 --> 00:23:56,999
就他可能是比较低效的
180
00:23:56,999 --> 00:24:03,167
但如果大多数都是ps的话,就说明一次能够处理四个float
181
00:24:03,167 --> 00:24:06,999
那这个生成代码就是比较高效的
182
00:24:07,042 --> 00:24:11,417
然后为什么我们需要这些指令呢?
183
00:24:11,417 --> 00:24:16,999
就我直接对单一一个去做加法不就好了吗?
184
00:24:16,999 --> 00:24:19,645
为什么四个打包到一起,因为这样更快呀,
185
00:24:19,669 --> 00:24:23,197
就是把四个打包到一起的话,
186
00:24:23,221 --> 00:24:34,578
它就可以大约就是相当于本来是四个标量的指令,它现在只要一个矢量的指令,
187
00:24:34,602 --> 00:24:37,999
从而它可以快四倍
188
00:24:37,999 --> 00:24:49,542
所以说它在对于那种以计算为主的程序,能够加快它的运行,编译器也会自动去对它进行优化
189
00:24:50,626 --> 00:24:59,500
然后编译器呢,它能够把你对着标量写的代码转换成一个针对矢量的代码
190
00:24:59,500 --> 00:25:10,999
我们待会会给出例子,这种技术称之为单指令多数据,然后英文简写就是SIMD
191
00:25:12,918 --> 00:25:19,498
对呀,没错,两个double 和四个float ,
192
00:25:19,522 --> 00:25:24,958
我们通常用的是四个float这个。
193
00:25:24,982 --> 00:25:31,999
然后就来看一看编译器还有哪些能做的优化
194
00:25:31,999 --> 00:25:37,709
首先一件事是要使它能够执行一些基本代数化
195
00:25:37,709 --> 00:25:50,417
比如看这个例子,这里c是a加b,d是a减b那么它们相加起来是不是b就会被抵消啊,是吧?
196
00:25:50,417 --> 00:25:53,999
然后a加a不是等于2a吗?
197
00:25:53,999 --> 00:25:57,398
那这个括号里就是一个2乘a,
198
00:25:57,422 --> 00:26:00,751
而2乘a除2自然就等于a。
199
00:26:00,775 --> 00:26:07,999
所以编译器很聪明,它直接把a作为返回值,也就是直接给它a了
200
00:26:08,999 --> 00:26:13,947
然后它还有一个点,就是如果我看到a和b都是常量,
201
00:26:13,971 --> 00:26:21,591
然后它们还相加起来,那我不是已经知道这两个常量了,我直接把它拿过来,
202
00:26:21,615 --> 00:26:27,250
然后相加变成42 ,它就直接优化成return 42了
203
00:26:29,974 --> 00:26:34,807
自己去测一下。
204
00:26:34,831 --> 00:26:40,809
然后它更疯狂的是,你甚至可以弄一个for 循环
205
00:26:40,834 --> 00:26:47,334
它看到这个初始值是一个常数,而i每次的值也都是常数
206
00:26:47,334 --> 00:26:54,999
那么它可以把1加到100给优化成直接返回5050 ,没问题的
207
00:26:56,999 --> 00:27:00,291
对呀,编译器其实很厉害啊,
208
00:27:00,315 --> 00:27:08,999
所以你要用好它的话,你甚至不需要影响可读性,你也能够提升性能
209
00:27:08,999 --> 00:27:16,405
然后当然编译器它没那么聪明,就是如果你把代码写的很复杂,
210
00:27:16,429 --> 00:27:20,211
比如用了一些STL容器库,
211
00:27:20,235 --> 00:27:27,425
那它就哎这个arr.push_back()是什么意思啊,它就停止思考了,
212
00:27:27,449 --> 00:27:34,999
它就会放弃优化,干脆就给你去一个个的去调用vector
213
00:27:34,999 --> 00:27:44,999
所以说像vector 这种会在堆上分配内存的容器,编译器通常是不会去进行优化
214
00:27:44,999 --> 00:27:53,999
所以说尽量把你的代码写的简单一点,编译器能够看得懂,它才能帮你优化
215
00:27:53,999 --> 00:27:59,612
如果你用一些很fancy 的,像这种它虽然和原来的结果是一样的,
216
00:27:59,636 --> 00:28:10,591
但你用这么复杂的话,编译器就认为我去优化这段很复杂的那个抽象语法树,它需要花很长时间,
217
00:28:10,615 --> 00:28:14,999
它就觉得不划算,它就放弃优化
218
00:28:14,999 --> 00:28:24,999
就是就是你不要把答案藏的太深,它的那个搜索范围是有限度的,它不会无限制的搜索
219
00:28:24,999 --> 00:28:34,999
所以你要把代码简化,从而它的搜索速度能变短,然后它就能成功的找到正确的优化
220
00:28:35,999 --> 00:28:42,999
看一下这个,这些东西都是存在堆上的
221
00:28:43,999 --> 00:28:48,999
然后这些是存在栈上的,明白吗?
222
00:28:51,999 --> 00:28:53,999
我去喝口水啊
223
00:29:01,125 --> 00:29:09,999
就是比如vector map set 这种,一般来说这些老的容器都是在堆上的
224
00:29:12,999 --> 00:29:15,459
为什么它们要分呢?
225
00:29:15,459 --> 00:29:18,999
就全部存在栈上不好吗?
226
00:29:18,999 --> 00:29:29,417
有区别。就是array, 它是固定大小的,它这是一个缺点,但是它的优点是它可以存在栈上
227
00:29:29,417 --> 00:29:32,999
那为什么vector 不能存在栈上呢?
228
00:29:32,999 --> 00:29:35,251
因为它可以动态的push_back() ,
229
00:29:35,275 --> 00:29:47,999
也就是它的大小是变化的,而栈上呢它只能一次性扩充一定的大小,而不能多次反复的进行扩充
230
00:29:47,999 --> 00:29:55,999
所以这时候就是像map set string 这种可以动态扩展的,它得存储在堆上
231
00:29:55,999 --> 00:30:09,999
也是在堆上,就是你一旦用了一个是,其中一个是存在堆上,那全部都上堆了,知道吧?
232
00:30:12,999 --> 00:30:20,092
就是像unique_ptr 这种,它肯定是去调用了new 和delete 嘛,所以也是在堆上。
233
00:30:20,116 --> 00:30:21,999
像pair呢
234
00:30:21,999 --> 00:30:30,025
就是你要判断最简单的办法就是sizeof(vector<array>),
235
00:30:30,049 --> 00:30:37,000
你算一下它的大小,它的大小是24 ,这个大小肯定容纳不了多少数据的
236
00:30:37,000 --> 00:30:43,959
所以说它这个存的只是一个指针和vector 的大小数据
237
00:30:43,959 --> 00:30:48,918
而如果你sizeof(array<float, 32>)的话,
238
00:30:48,942 --> 00:30:54,999
你会发现它等于32乘4 ,也就是说它的大小是很大的
239
00:30:54,999 --> 00:31:06,042
你只要算这个sizeof ,你就知道它存的是一个指针还是实际的数据。没法用
240
00:31:06,042 --> 00:31:08,257
左值?
241
00:31:08,281 --> 00:31:12,438
array怎么没法用左值啊?
242
00:31:12,462 --> 00:31:19,358
我记得这种也可以的吧,这种也可以的吧,
243
00:31:19,382 --> 00:31:22,375
tuple也可以用左值的呀
244
00:31:24,083 --> 00:31:30,999
没理解你的意思,像这种是不行的,但tuple 肯定是可以用左值的
245
00:31:33,876 --> 00:31:42,999
总之就是刚才那个如果你改成array是不是能优化成功呢?
246
00:31:42,999 --> 00:31:49,971
试试看,就是我现在改了,哎,还是优化失败,那我再改一下,
247
00:31:49,995 --> 00:31:54,872
改用手写的reduce,它还是优化失败。
248
00:31:54,896 --> 00:32:00,798
我再改呢,把这个100改成10呢,它优化成功了,
249
00:32:00,822 --> 00:32:03,974