热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

CPUCache原理与示例

CPUCache原理与示例基础知识现在的CPU多核技术,都会有几级缓存,老的CPU会有两级内存(L1和L2),

CPU Cache原理与示例
基础知识
现在的 CPU 多核技术,都会有几级缓存,老的 CPU 会有两级内存(L1 和 L2),新的CPU会有三级内存(L1,L2,L3 ),如下图所示:
在这里插入图片描述

其中:
 L1 缓存分成两种,一种是指令缓存,一种是数据缓存。L2 缓存和 L3 缓存不分指令和数据。
 L1 和 L2 缓存在每一个 CPU 核中,L3 则是所有 CPU 核心共享的内存。
 L1、L2、L3 的越离CPU近就越小,速度也越快,越离 CPU 远,速度也越慢。
 再往后面就是内存,内存的后面就是硬盘。看一些速度:
 L1 的存取速度:4 个CPU时钟周期
 L2 的存取速度:11 个CPU时钟周期
 L3 的存取速度:39 个CPU时钟周期
 RAM内存的存取速度 :107 个CPU时钟周期
L1 的速度是 RAM 的 27 倍,但是 L1/L2 的大小基本上也就是 KB 级别的,L3 会是 MB 级别的。例如:Intel Core i7-8700K ,一个 6 核的 CPU,每核上的 L1 是 64KB(数据和指令各 32KB),L2 是 256K,L3 有 2MB(苹果电脑是 Intel Core i9-8950HK,和Core i7-8700K 的Cache大小一样)。
数据就从内存向上,先到 L3,再到 L2,再到 L1,最后到寄存器进行 CPU 计算。为什么会设计成三层?这里有下面几个方面的考虑:
 一个方面是物理速度,如果要更大的容量就需要更多的晶体管,除了芯片的体积会变大,更重要的是大量的晶体管会导致速度下降,因为访问速度和要访问的晶体管所在的位置成反比,当信号路径变长时,通信速度会变慢。这是物理问题。
 另外一个问题是,多核技术中,数据的状态需要在多个CPU中进行同步,cache 和RAM 的速度差距太大,多级不同尺寸的缓存有利于提高整体的性能。
 这个世界永远是平衡的,一面变得有多光鲜,另一面也会变得有多黑暗。建立这么多级的缓存,一定就会引入其它的问题,这里有两个比较重要的问题,
 一个是比较简单的缓存的命中率的问题。
 另一个是比较复杂的缓存更新的一致性问题。
尤其是第二个问题,在多核技术下,这就很像分布式的系统了,要对多个地方进行更新。
缓存的命中
在说明这两个问题之前。需要了解一个术语 Cache Line。缓存就是把后面的数据加载到离自己近的地方,对于 CPU 来说,不会一个字节一个字节的加载的,非常没有效率,都是要一块一块的加载的,对于这样的一块一块的数据单位,术语叫 Cache Line。
一般来说,一个主流的 CPU 的 Cache Line 是 64 Bytes(也有的CPU用32Bytes和128Bytes),64 Bytes也就是 16 个 32 位的整型,这就是 CPU 从内存中捞数据的最小数据单位。
比如:Cache Line是最小单位(64Bytes),先把 Cache 分布多个 Cache Line,比如:L1 有 32KB,32KB/64B = 512 个 Cache Line。
一方面,缓存需要把内存里的数据放进来,英文叫 CPU Associativity。Cache 的数据放置的策略决定了内存中的数据块会拷贝到 CPU Cache 中的哪个位置上,因为 Cache 的大小远远小于内存,需要有一种地址关联的算法,能够让内存中的数据可以被映射到 Cache 中来。这个有点像内存地址从逻辑地址向物理地址映射的方法,但不完全一样。
有如下的一些方法。
 一种方法是,任何一个内存地址的数据可以被缓存在任何一个 Cache Line 里,这种方法是最灵活的,但是,如果要知道一个内存是否存在于 Cache 中,就需要进行 O(n) 复杂度的 Cache 遍历,这是很没有效率的。
 另一种方法,为了降低缓存搜索算法,需要使用像Hash Table这样的数据结构,最简单的hash table就是做求模运算,比如: L1 Cache 有 512 个 Cache Line,公式:(内存地址 mod 512)* 64 就可以直接找到所在的Cache地址的偏移了。但是,这样的方式需要程序对内存地址的访问要非常平均,不然冲突就会非常严重。这成了一种非常理想的情况了。
 为了避免上述的两种方案的问题,就要容忍一定的hash冲突,出现了 N-Way 关联。把连续的N 个 Cache Line 绑成一组,先把找到相关的组,再在这个组内找到相关的 Cache Line。这叫 Set Associativity。如下图所示。
在这里插入图片描述

对于 N-Way 组关联,可能有点不好理解,这里个例子,并多说一些细节(不然后面的代码会不能理解),Intel 大多数处理器的 L1 Cache 都是 32KB,8-Way 组相联,Cache Line 是 64 Bytes。这意味着,
 32KB的可以分成,32KB / 64 = 512 条 Cache Line。
 因为有8 Way,于是会每一Way 有 512 / 8 = 64 条 Cache Line。
 每一路就有 64 x 64 = 4096 Byts 的内存。
 为了方便索引内存地址,
 Tag:每条 Cache Line 前都会有一个独立分配的 24 bits来存的 tag,其就是内存地址的前24bits
 Index:内存地址后续的 6 个 bits 则是在这一 Way 的是Cache Line 索引,2^6 = 64 刚好可以索引64条Cache Line
 Offset:再往后的 6bits 用于表示在 Cache Line 里的偏移量
如下图所示:(图片来自《Cache: a place for concealment and safekeeping》)
当拿到一个内存地址的时候,先拿出中间的 6bits,找到是哪组。
在这里插入图片描述

然后,在这一个 8 组的 cache line 中,再进行 O(n) n=8 的遍历,主是要匹配前 24bits 的 tag。如果匹配中了,就算命中,如果没有匹配到,那就是 cache miss,如果是读操作,就需要进向后面的缓存进行访问了。
L2/L3 同样是这样的算法。淘汰算法有两种,一种是随机一种是 LRU。现在一般都是以 LRU 的算法(通过增加一个访问计数器实现)。
在这里插入图片描述

这也意味着:
 L1 Cache 可映射 36bits 的内存地址,一共 2^36 = 64GB 的内存
 当 CPU 要访问一个内存的时候,通过这个内存中间的 6bits 定位是哪个 set,通过前 24bits 定位相应的Cache Line。
 就像一个 hash Table 的数据结构一样,先是 O(1)的索引,然后进入冲突搜索。
 因为中间的 6bits 决定了一个同一个 set,所以,对于一段连续的内存来说,每隔 4096 的内存会被放在同一个组内,导致缓存冲突。
此外,当有数据没有命中缓存的时候,CPU 就会以最小为 Cache Line 的单元向内存更新数据。当然,CPU 并不一定只是更新 64Bytes,因为访问主存实在是太慢了,所以,一般都会多更新一些。好的 CPU 会有一些预测的技术,如果找到一种 pattern 的话,就会预先加载更多的内存,包括指令也可以预加载。
这叫 Prefetching 技术 (参看,Wikipedia 的 Cache Prefetching 和 纽约州立大学的 Memory Prefetching)。比如,在for-loop访问一个连续的数组,步长是一个固定的数,内存就可以做到prefetching。(注:指令也是以预加载的方式执行)
了解这些细节,会有利于知道在什么情况下有可以导致缓存的失效。
缓存的一致性
对于主流的 CPU,缓存的写操作基本上是两种策略,
 一种是 Write Back,写操作只要在 cache 上,然后再 flush 到内存上。
 一种是 Write Through,写操作同时写到 cache 和内存上。
主流的 CPU(如:Intel Core i7/i9)采用的是 Write Back 的策略,因为直接写内存实在是太慢了。
如果有一个数据 x 在 CPU 第 0 核的缓存上被更新了,其它 CPU 核上对于这个数据 x 的值也要被更新,这就是缓存一致性的问题。(当然,对于上层的程序不用关心 CPU 多个核的缓存是怎么同步的,这对上层的代码来说都是透明的)
一般来说,在 CPU 硬件上,会有两种方法来解决这个问题。
 Directory 协议。这种方法的典型实现是要设计一个集中式控制器,主存储器控制器的一部分。其中有一个目录存储在主存储器中,其中包含有关各种本地缓存内容的全局状态信息。当单个 CPU Cache 发出读写请求时,这个集中式控制器会检查并发出必要的命令,以在主存和 CPU Cache之间或在 CPU Cache自身之间进行数据同步和传输。
 Snoopy 协议。这种协议更像是一种数据通知的总线型的技术。CPU Cache 通过这个协议可以识别其它Cache上的数据状态。如果有数据共享的话,可以通过广播机制将共享数据的状态通知给其它 CPU Cache。这个协议要求每个 CPU Cache 都可以窥探数据事件的通知并做出相应的反应。如下图所示,有一个 Snoopy Bus 的总线。
在这里插入图片描述

因为 Directory 协议是一个中心式的,会有性能瓶颈,会增加整体设计的复杂度。而 Snoopy 协议更像是微服务+消息通讯,所以,现在基本都是使用 Snoopy 的总线的设计。
在分布式系统中一般用 Paxos/Raft 这样的分布式一致性的算法。
在 CPU 的微观世界里,不必使用这样的算法,因为 CPU 的多个核的硬件不必考虑网络会断会延迟的问题。所以,CPU 的多核心缓存间的同步的核心,就是要管理好数据的状态。
这里介绍几个状态协议,先从最简单的开始,MESI 协议,这个协议跟那个著名的足球运动员梅西没什么关系,其主要表示缓存数据有四个状态:Modified(已修改), Exclusive(独占的),Shared(共享的),Invalid(无效的)。
这些状态的状态机如下所示(、这个图就是想告诉状态控制有多复杂):
在这里插入图片描述

下面是个示例(如果想看一下动画演示的话,这里有一个网页(MESI Interactive Animations),可以进行交互操作,这个动画演示中使用的 Write Through 算法):
MESI 这种协议在数据更新后,会标记其它共享的 CPU 缓存的数据拷贝为 Invalid 状态,然后当其它 CPU 再次read 的时候,就会出现 cache miss 的问题,再从内存中更新数据,意味着 20 倍速度的降低。
能不能直接从隔壁的 CPU 缓存中更新?可以,这就可以增加很多速度了,但是状态控制也就变麻烦了。需要多来一个状态:Owner(宿主),用于标记,更新数据源。出现了 MOESI 协议
MOESI 协议的状态机和演示示例就不写了(有兴趣可以上Berkeley上看看相关的课件),只需要理解MOESI协议允许 CPU Cache 间同步数据,降低了对内存的操作,性能是非常大的提升,但是控制逻辑也非常复杂。
与 MOESI 协议类似的一个协议是 MESIF,其中的 F 是 Forward,同样是把更新过的数据转发给别的 CPU Cache。但是,MOESI 中的 Owner 状态 和MESIF 中的 Forward 状态有一个非常大的不一样——Owner 状态下的数据是 dirty 的,没有写回内存,Forward 状态下的数据是 clean的,可以丢弃,不用另行通知。
需要说明的是,AMD 用 MOESI,Intel 用 MESIF。F 状态主要是针对 CPU L3 Cache 设计的(L3 是所有 CPU 核心共享的)。
程序性能
看一下对于程序的影响。
示例一
首先,假设有一个64M长的数组,设想一下下面的两个循环:
const int LEN = 6410241024;
int arr = new int[LEN];
for (int i = 0; i arr[i] = i;
for (int i = 0; i arr[i] = i;
按想法来看,第二个循环要比第一个循环少4倍的计算量,其应该也是要快4倍的。但实际跑下来并不是,在机器上,第一个循环需要 127 毫秒,第二个循环则需要 121 毫秒,相差无几。
这里最主要的原因就是 Cache Line,因为 CPU 会以一个 Cache Line 64Bytes 最小时单位加载,也就是 16 个 32bits 的整型,所以,无论步长是 2 还是 8,都差不多。后面的乘法其实是不耗 CPU 时间的。
示例二
再来看一个与缓存命中率有关的代码,以一定的步长increment 来访问一个连续的数组。
for (int i &#61; 0; i <10000000; i&#43;&#43;)
{
for (int j &#61; 0; j {
memory[j] &#43;&#61; j;
}
}
测试一下&#xff0c;在下表中&#xff0c; 表头是步长&#xff0c;也就是每次跳多少个整数&#xff0c;而纵向是这个数组可以跳几次&#xff08;可以理解为要几条 Cache Line&#xff09;&#xff0c;于是表中的任何一项代表了这个数组有多少&#xff0c;而且步长是多少。
比如&#xff1a;横轴是 512&#xff0c;纵轴是4&#xff0c;意思是&#xff0c;这个数组有 4
512 &#61; 2048 个长度&#xff0c;访问时按512步长访问&#xff0c;也就是访问其中的这几项&#xff1a;[0, 512, 1024, 1536] 这四项。
表中同的项是&#xff0c;是循环 1000 万次的时间&#xff0c;单位是“微秒”&#xff08;除以1000后是毫秒&#xff09;
| count | 1 | 16 | 512 | 1024 | ------------------------------------------ | 1 | 17539 | 16726 | 15143 | 14477 | | 2 | 15420 | 14648 | 13552 | 13343 | | 3 | 14716 | 14463 | 15086 | 17509 | | 4 | 18976 | 18829 | 18961 | 21645 | | 5 | 23693 | 23436 | 74349 | 29796 | | 6 | 23264 | 23707 | 27005 | 44103 | | 7 | 28574 | 28979 | 33169 | 58759 | | 8 | 33155 | 34405 | 39339 | 65182 | | 9 | 37088 | 37788 | 49863 |156745 | | 10 | 41543 | 42103 | 58533 |215278 | | 11 | 47638 | 50329 | 66620 |335603 | | 12 | 49759 | 51228 | 75087 |305075 | | 13 | 53938 | 53924 | 77790 |366879 | | 14 | 58422 | 59565 | 90501 |466368 | | 15 | 62161 | 64129 | 90814 |525780 | | 16 | 67061 | 66663 | 98734 |440558 | | 17 | 71132 | 69753 |171203 |506631 | | 18 | 74102 | 73130 |293947 |550920 |
可以看到&#xff0c;从 [9&#xff0c;1024] 以后&#xff0c;时间显著上升。包括 [17&#xff0c;512] 和 [18,512] 也显著上升。这是因为&#xff0c;机器的 L1 Cache 是 32KB, 8 Way 的&#xff0c;前面说过&#xff0c;8 Way 的有 64 组&#xff0c;每组 8 个 Cache Line&#xff0c;当 for-loop步长超过 1024 个整型&#xff0c;也就是正好 4096 Bytes 时&#xff0c;也就是导致内存地址的变化是变化在高位的 24bits 上&#xff0c;
而低位的1 2bits 变化不大&#xff0c;尤其是中间6bits没有变化&#xff0c;导致全部命中同一组 set&#xff0c;导致大量的 cache 冲突&#xff0c;导致性能下降&#xff0c;时间上升。[16, 512]也是一样的&#xff0c;其中的几步开始导致L1 Cache开始冲突失效。
示例三
接下来&#xff0c;再来看个示例。下面是一个二维数组的两种遍历方式&#xff0c;一个逐行遍历&#xff0c;一个是逐列遍历&#xff0c;这两种方式在理论上来说&#xff0c;寻址和计算量都是一样的&#xff0c;执行时间应该也是一样的。
const int row &#61; 1024; const int col &#61; 512 int matrix[row][col]; //逐行遍历 int sum_row&#61;0; for(int _r&#61;0; _r 然而&#xff0c;并不是&#xff0c;在机器上&#xff0c;得到下面的结果。
 逐行遍历&#xff1a;0.081ms
 逐列遍历&#xff1a;1.069ms
执行时间有十几倍的差距。其中的原因&#xff0c;就是逐列遍历对于 CPU Cache 的运作方式并不友好&#xff0c;所以&#xff0c;付出巨大的代价。
示例四
接下来&#xff0c;来看一下多核下的性能问题&#xff0c;参看如下的代码。两个线程在操作一个数组的两个不同的元素&#xff08;无需加锁&#xff09;&#xff0c;线程循环1000万次&#xff0c;做加法操作。在下面的代码中&#xff0c;高亮了一行&#xff0c;就是p2指针&#xff0c;要么是p[1]&#xff0c;或是 p[30]&#xff0c;理论上来说&#xff0c;无论访问哪两个数组元素&#xff0c;都应该是一样的执行时间。
void fn (int
data) { for(int i &#61; 0; i <10
1024*1024; &#43;&#43;i) *data &#43;&#61; rand(); } int p[32]; int *p1 &#61; &p[0]; int *p2 &#61; &p[1]; // int p2 &#61; &p[30]; thread t1(fn, p1); thread t2(fn, p2);
然而&#xff0c;并不是&#xff0c;在机器上执行下来的结果是&#xff1a;
 对于 p[0] 和 p[1] &#xff1a;560ms
 对于 p[0] 和 p[30]&#xff1a;104ms
这是因为 p[0] 和 p[1] 在同一条 Cache Line 上&#xff0c;而 p[0] 和 p[30] 则不可能在同一条Cache Line 上 &#xff0c;CPU 的缓存最小的更新单位是 Cache Line&#xff0c;所以&#xff0c;这导致虽然两个线程在写不同的数据&#xff0c;但是因为这两个数据在同一条 Cache Line 上&#xff0c;就会导致缓存需要不断进在两个 CPU 的 L1/L2 中进行同步&#xff0c;从而导致了 5 倍的时间差异。
示例五
接下来&#xff0c;再来看一下另外一段代码&#xff1a;统计一下一个数组中的奇数个数&#xff0c;但是这个数组太大了&#xff0c;希望可以用多线程来完成这个统计。下面的代码中&#xff0c;为每一个线程传入一个 id &#xff0c;然后通过这个 id 来完成对应数组段的统计任务。这样可以加快整个处理速度。
int total_size &#61; 16 * 1024 * 1024; //数组长度
int
test_data &#61; new test_data[total_size]; //数组
int nthread &#61; 6; //线程数&#xff08;因为机器是6核的&#xff09;
int result[nthread]; //收集结果的数组
void thread_func (int id)
{
result[id] &#61; 0;
int chunk_size &#61; total_size / nthread &#43; 1;
int start &#61; id * chunk_size;
int end &#61; min(start &#43; chunk_size, total_size);
for ( int i &#61; start; i {
if (test_data[i] % 2 !&#61; 0 )
&#43;&#43;result[id];
}
}
然而&#xff0c;在执行过程中&#xff0c;会发现&#xff0c;6 个线程居然跑不过 1 个线程。因为根据上面的例子知道 result[]数组中的数据在一个 Cache Line 中&#xff0c;所以&#xff0c;所有的线程都会对这个 Cache Line 进行写操作&#xff0c;导致所有的线程都在不断地重新同步result[] 所在的 Cache Line&#xff0c;所以&#xff0c;导致 6 个线程还跑不过一个线程的结果。这叫False Sharing。
优化也很简单&#xff0c;使用一个线程内的变量。
void thread_func (int id)
{
result[id] &#61; 0;
int chunk_size &#61; total_size / nthread &#43; 1;
int start &#61; id * chunk_size;
int end &#61; min(start &#43; chunk_size, total_size);
int c &#61; 0; //使用临时变量&#xff0c;没有cache line的同步了
for ( int i &#61; start; i {
if (test_data[i] % 2 !&#61; 0 ) &#43;&#43;c;
}
result[id] &#61; c;
}
把两个程序分别在 1 到 32 个线程上跑一下&#xff0c;得出的结果画一张图如下所示&#xff08;横轴是线程数&#xff0c;纵轴是完成统的时间&#xff0c;单位是微秒&#xff09;&#xff1a;
在这里插入图片描述

上图中&#xff0c;可以看到&#xff0c;灰色的曲线就是第一种方法&#xff0c;橙色的就是第二种&#xff08;用局部变量的&#xff09;方法。当只有一个线程的时候&#xff0c;两个方法相当&#xff0c;基本没有什么差别&#xff0c;但是在线程数增加时&#xff0c;会发现&#xff0c;第二种方法的性能提高的非常快。直到到达 6 个线程的时候&#xff0c;开始变得稳定&#xff08;前面说过&#xff0c;CPU 是6核的&#xff09;。
第一种方法无论加多少线程也没有办法超过第二种方法。因为第一种方法不是 CPU Cache 友好的。也就是说&#xff0c;第二种方法&#xff0c;只要 CPU 核数足够多&#xff0c;就可以做到线性的性能扩展&#xff0c;让每一个 CPU 核都跑起来&#xff0c;第一种则不能。

参考链接&#xff1a;
https://zhuanlan.zhihu.com/p/445961133
https://mp.weixin.qq.com/s/s9w–YRkyAvQi4LQcenq4g


推荐阅读
  • 篇首语:本文由编程笔记#小编为大家整理,主要介绍了软件测试知识点之数据库压力测试方法小结相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 在2022年,随着信息化时代的发展,手机市场上出现了越来越多的机型选择。如何挑选一部适合自己的手机成为了许多人的困扰。本文提供了一些配置及性价比较高的手机推荐,并总结了选择手机时需要考虑的因素,如性能、屏幕素质、拍照水平、充电续航、颜值质感等。不同人的需求不同,因此在预算范围内找到适合自己的手机才是最重要的。通过本文的指南和技巧,希望能够帮助读者节省选购手机的时间。 ... [详细]
  • 网卡工作原理及网络知识分享
    本文介绍了网卡的工作原理,包括CSMA/CD、ARP欺骗等网络知识。网卡是负责整台计算机的网络通信,没有它,计算机将成为信息孤岛。文章通过一个对话的形式,生动形象地讲述了网卡的工作原理,并介绍了集线器Hub时代的网络构成。对于想学习网络知识的读者来说,本文是一篇不错的参考资料。 ... [详细]
  • 基于移动平台的会展导游系统APP设计与实现的技术介绍与需求分析
    本文介绍了基于移动平台的会展导游系统APP的设计与实现过程。首先,对会展经济和移动互联网的概念进行了简要介绍,并阐述了将会展引入移动互联网的意义。接着,对基础技术进行了介绍,包括百度云开发环境、安卓系统和近场通讯技术。然后,进行了用户需求分析和系统需求分析,并提出了系统界面运行流畅和第三方授权等需求。最后,对系统的概要设计进行了详细阐述,包括系统前端设计和交互与原型设计。本文对基于移动平台的会展导游系统APP的设计与实现提供了技术支持和需求分析。 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • 本文由编程笔记#小编整理,主要介绍了关于数论相关的知识,包括数论的算法和百度百科的链接。文章还介绍了欧几里得算法、辗转相除法、gcd、lcm和扩展欧几里得算法的使用方法。此外,文章还提到了数论在求解不定方程、模线性方程和乘法逆元方面的应用。摘要长度:184字。 ... [详细]
  • 如何使用代理服务器进行网页抓取?
    本文介绍了如何使用代理服务器进行网页抓取,并探讨了数据驱动对竞争优势的重要性。通过网页抓取,企业可以快速获取并分析大量与需求相关的数据,从而制定营销战略。同时,网页抓取还可以帮助电子商务公司在竞争对手的网站上下载数百页的有用数据,提高销售增长和毛利率。 ... [详细]
  • Android自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
  • 上图是InnoDB存储引擎的结构。1、缓冲池InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可以看作是基于磁盘的数据库系统。在数据库系统中,由于CPU速度 ... [详细]
author-avatar
787849993_8c653d
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有