深入理解CPU缓存一致性

news2024/11/17 3:29:43

存储体系结构

速度快的存储硬件成本高、容量小,速度慢的成本低、容量大。为了权衡成本和速度,计算机存储分了很多层次,有寄存器、L1 cache、L2 cache、L3 cache、主存(内存)和硬盘等。

根据程序的空间局部性和时间局部性原理,缓存命中率可以达到 70~90% 。因此,增加缓存可以让整个存储系统的性能接近寄存器,并且每字节的成本都接近内存,所以缓存是存储体系结构的灵魂。
在这里插入图片描述

缓存原理

缓存的工作原理

cache line(缓存行)是缓存进行管理的最小存储单元,也叫缓存块,每个 cache line 包含 Flag、Tag 和 Data ,通常 Data 大小是 64 字节,但不同型号 CPU 的 Flag 和 Tag 可能不相同。从内存向缓存加载数据是按整个缓存行加载的,一个缓存行和一个相同大小的内存块对应。

在这里插入图片描述

缓存是按照矩阵方式排列(M × N),横向是组(Set),纵向是路(Way)。每一个元素是缓存行(cache line)。

那么给定一个虚拟地址 addr 如何在缓存中定位它呢?首先把它所在的组号找到,即:

//右移6位是因为 Block Offset 占 addr 的低 6 位,Data 为 64 字节
Set Index = (addr >> 6) % M;

然后遍历该组所有的路,找到 cache line 中的 Tag 与 addr 中 Tag 相等为止,所有路都没有匹配成功,那么缓存未命中。整个缓存容量 = 组数 × 路数 × 缓存行大小。

缓存行替换策略

目前最常用的缓存替换策略是最近最少使用算法(Least Recently Used ,LRU)或者是类似 LRU 的算法。

LRU 算法比较简单,如图3,缓存有 4 路,并且访问的地址都哈希到了同一组,访问顺序是 D1、D2、D3、D4 和 D5,那么 D1 会被 D5 替换掉。算法的实现方式有很多种,最简单的实现方式是位矩阵。

首先,定义一个行、列都与缓存路数相同的矩阵。当访问某个路对应的缓存行时,先将该路对应的所有行置为 1,然后再将该路对应的所有列置为 0。

最近最少使用的缓存行所对应的矩阵行中 1 的个数最少,最先被替换出去。

在这里插入图片描述

缓存缺失

缓存缺失就是缓存未命中,需要把内存中数据加载到缓存,所以运行速度会变慢。

拿电脑来测试,L1d 的缓存大小是 32KB(32768B),8路,缓存行大小 64B,缓存组数 = 32 × 1024 ÷ 8 ÷ 64 = 64

运行下面代码

char *a = new char(64 * 64 * 8); //32768B 对应64行 一行64组
for(int i = 0; i < 20000000; i++) 
    for(int j = 0; j < 32768; j += 4096) 
        a[j]++;

循环 160000000 次,耗时 301 ms。除了第一次未命中缓存,后面每次读写数据都能命中缓存。调整上面的代码,并运行

char *a = new char(64 * 64 * 8 * 2); //65536B
for(int i = 0; i < 10000000; i++)
    for(int j = 0; j < 65536; j += 4096)
        a[j]++;

本次耗时 959 ms。每一次读写数据都没有命中缓存,所以耗时增加了 2 倍。

程序局部性

程序局部性就是读写内存数据时读写连续的内存空间,目的是让缓存可以命中,减少缓存缺失导致替换的开销。

int M = 10000, N = 10000;
char (*a)[N] = (char(*)[N])calloc(M * N, sizeof(char));
for(int i = 0; i < M; i++)
    for(int j = 0; j < N; j++)
        a[i][j]++;

耗时 314 ms。利用了程序局部性原理,缓存命中率高。以下代码耗时 1187 ms。没有利用程序局部性原理,缓存命中率低,所以耗时增加了 2 倍。

int M = 10000, N = 10000;
char (*a)[N] = (char(*)[N])calloc(M * N, sizeof(char));
for(int j = 0; j < N; j++)
    for(int i = 0; i < M; i++)
        a[i][j]++;

伪共享

当两个线程同时各自修改两个相邻的变量,由于缓存是按缓存行来整体组织的,当一个线程对缓存行中数据执行写操作时,必须通知其他线程该缓存行失效,导致另一个线程从缓存中读取其想修改的数据失败,必须从内存重新加载,导致性能下降。

struct S {
    long long a;
    long long b;
} s;
std::thread t1([&]() {
    for(int i = 0; i < 100000000; i++)
        s.a++;
});
std::thread t2([&]() {
    for(int i = 0; i < 100000000; i++)
        s.b++;
});

耗时 512 ms,是两个线程互相影响,使对方的缓存行失效,导致直接从内存读取数据。解决办法是对上面代码做修改,通过 long long noop[8] 把两个数据(a 和 b)划分到两个不同的缓存行中,不再互相使对方的缓存失效,所以速度变快了。修改后耗时181ms。

struct S {
    long long a;
    long long noop[8];
    long long b;
} s;

缓存一致性协议

缓存写策略

从缓存和内存的更新关系来看,分为:

写回(write-back) 对缓存的修改不会立刻传播到内存,只有当缓存行被替换时,这些被修改的缓存行才会写回并覆盖内存中过时的数据。

写直达(write through) 缓存中任何一个字节的修改,都会立刻穿透缓存直接传播到内存,这种比较耗时。

从写缓存时 CPU 之间的更新策略来看,分为:

写更新(Write Update) 每次缓存写入新的值,该核心必须发起一次总线请求,通知其他核心更新他们缓存中对应的值。
坏处:写更新会占用很多总线带宽;
好处:其他核心能立刻获得最新的值。

写无效(Write Invalidate) 每次缓存写入新的值,都将其他核心缓存中对应的缓存行置为无效。
坏处:当其他核心再次访问该缓存时,发现缓存行已经失效,必须从内存中重新载入最新的数据;
好处:多次写操作只需发一次总线事件,第一次写已经将其他核心缓存行置为无效,之后的写不必再更新状态,这样可以有效地节省核心间总线带宽。

从写缓存时数据是否被加载来看,分为:

写分配(Write Allocate) 在写入数据前将数据读入缓存。当缓存块中的数据在未来读写概率较高,也就是程序空间局部性较好时,写分配的效率较好。
写不分配(Not Write Allocate) 在写入数据时,直接将数据写入内存,并不先将数据块读入缓存。当数据块中的数据在未来使用的概率较低时,写不分配性能较好。

MESI 协议

MESI协议是基于失效的缓存一致性协议,是支持写回缓存的最常用协议。为了解决多个核心之间的数据传播问题,提出了总线嗅探(Bus Snooping)策略。本质上就是把所有的读写请求都通过总线(Bus)广播给所有的核心,然后让各个核心去嗅探这些请求,再根据本地的状态进行响应。

它的名字来自四种状态:修改(Modified),专有(Exclusive),共享(Shared),和无效(Invalid)。

修改(Modified):缓存行被某个处理器独占修改过,并且与主存中的数据不同步。只有该处理器可以访问这个缓存行,其他处理器都不能访问。

专有(Exclusive):缓存行被某个处理器独占,但没有被修改过,与主存中的数据是一致的。只有该处理器可以访问这个缓存行,但如果其他处理器想要访问,可以直接读取主存中的数据。

共享(Shared):缓存行可以被多个处理器同时读取,但没有被修改过,与主存中的数据是一致的。多个处理器都可以同时访问这个缓存行。

无效(Invalid):缓存行的数据是无效的,不能被使用。如果某个处理器需要使用这个数据,它必须从主存中重新读取。

当一个处理器要读取或写入一个数据时,MESI 协议会检查这个数据在缓存中的状态,并根据情况进行以下操作:

读取操作:

  • 如果数据在缓存中是无效的,处理器会从主存中读取数据,并将状态设置为共享或专有。
  • 如果数据在缓存中是共享或专有的,处理器可以直接读取缓存中的数据。

写入操作:

  • 如果数据在缓存中是无效或共享的,处理器会将数据从主存中读取并进行修改,将状态设置为修改。
  • 如果数据在缓存中是专有的,处理器可以直接修改缓存中的数据,并将状态改为修改。

内存屏障

基本概念

编译器和处理器都必须遵守重排序规则。在单处理器的情况下,不需要任何额外的操作便能保持正确的顺序。但是对于多处理器来说,保证一致性通常需要增加内存屏障指令。即使编译器可以优化掉字段的访问(例如因为未使用加载到的值),编译器仍然需要生成内存屏障,就好像字段访问仍然存在一样(可以单独将内存屏障优化掉)。

内存屏障只与内存模型中的高级概念(例如 acquire 和 release)间接相关。内存屏障指令只直接控制 CPU 与其缓存的交互,以及它的写缓冲区(持有等待刷新到内存的数据的存储)和它的用于等待加载或推测执行指令的缓冲。这些影响可能导致缓存、主内存和其他处理器之间的进一步交互。

几乎所有的处理器都至少支持一个粗粒度的屏障指令(通常称为 Fence,也叫全屏障),它保证了严格的有序性:在 Fence 之前的所有读操作(load)和写操作(store)先于在 Fence 之后的所有读操作(load)和写操作(store)执行完。对于任何的处理器来说,这通常都是最耗时的指令之一(它的开销通常接近甚至超过原子操作指令)。大多数处理器还支持更细粒度的屏障指令。

写缓冲与写屏障

严格按照MESI协议,核心0 在修改本地缓存之前,需要向其他核心发送 Invalid 消息,其他核心收到消息后,使他们本地对应的缓存行失效,并返回 Invalid acknowledgement 消息,核心0 收到后修改缓存行。这里核心0 等待其他核心返回确认消息的时间对核心来说是漫长的。

在这里插入图片描述

为了解决这个问题,引入了 Store Buffer ,当核心想修改缓存时,直接写入 Store Buffer ,无需等待,继续处理其他事情,由 Store Buffer 完成后续工作。

在这里插入图片描述

这样一来写的速度加快了,但是引来了新问题,下面代码的 bar 函数中的断言可能会失败。

int a = 0, b = 0;
// CPU0
void foo() {
    a = 1;
    b = 1;
}
// CPU1
void bar() {
    while (b == 0) continue;
    assert(a == 1);
}

第一种情况:CPU 为了提升运行效率和提高缓存命中率,采用了乱序执行;

第二种情况:Store Buffer 在写入时,b 所对应的缓存行是 专有(Exclusive)状态,a 所对应的缓存行是 共享(Shared) 状态,因为对 b 的修改不需要核心间同步,但是修改 a 则需要,也就是 b 会先写入缓存。与之对应 CPU1 中 a 是 共享(Shared) 状态,b 是 无效(Invalid)状态,由于 b 所对应的缓存区域是无效(Invalid)状态,它就会向总线发出 BusRd 请求,那么 CPU1 就会先把 b 的最新值读到本地,完成变量 b 值的更新,但是从缓存直接读取 a 值是 0 。

举一个更极端的例子

// CPU0
void foo() {
    a = 1;
    b = a;
}

第一种情况不会发生了,原因是代码有依赖,不会乱序执行。但由于 Store Buffer 的存在,第二种情况仍然可能发生,原因同上。

为了解决上面问题,引入了内存屏障,屏障的作用是前边的读写操作未完成的情况下,后面的读写操作不能发生。这就是 Arm 上 dmb 指令的由来,它是数据内存屏障(Data Memory Barrier)的缩写。

int a = 0, b = 0; 
// CPU0
void foo() {
    a = 1;
    smp_mb(); //内存屏障,各CPU平台实现不一样
    b = 1;
}
// CPU1
void bar() {
    while (b == 0) continue;
    assert(a == 1);
}

加上内存屏障后,保证了 a 和 b 的写入缓存顺序。

总的来说,Store Buffer 提升了写性能,但放弃了缓存的顺序一致性,这种现象称为弱缓存一致性。通常情况下,多个 CPU 一起操作同一个变量的情况是比较少的,所以 Store Buffer 可以大幅提升程序的性能。但在需要核间同步的情况下,还是需要通过手动添加内存屏障来保证缓存一致性。

上面解决了核间同步的写问题,但是核间同步还有一个瓶颈,那就是读。

失效队列与读屏障

前面引入 Store Buffer 提升了写入速度,那么 invalid 消息确认速度相比起来就慢了,带来了速度不匹配,很容易导致 Store Buffer 的内容还没及时写到缓存里,自己就满了,从而失去了加速的作用。

为了解决这个问题,又引入了 Invalid Queue。收到 Invalid 消息的核心立刻返回 Invalid acknowledgement 消息,然后把 Invalid 消息加入 Invalid Queue ,等到空闲的时候再去处理 Invalid 消息。

在这里插入图片描述

运行上面增加内存屏障的代码,断言可能还是失败。

核心0 中 a 所对应的缓存行是 S 状态,b 所对应的缓存行是 E 状态;核心1中 a 所对应的缓存行是 S 状态,b 所对应的缓存行是 I 状态;

  • 因为有内存屏障在,a 和 b的写入缓存的顺序不会乱。
  • a 先向其他核心发送 Invalid 消息,并且等待 Invalid 确认消息;
  • Invalid 消息先入 核心1 对应的 Invalid Queue 并立刻返回确认消息,等待 核心1 处理;
  • 核心0 收到确认消息后把 a 写入缓存,继续处理 b 的写入,由于 b 是 E 状态,直接写入缓存;
  • 核心1 发送 BusRd 消息,读取到新的 b 值,然后获取 a(S 状态)值是0,因为使其无效的消息还在 Invalid Queue 中,断言失败。
  • 引入 Invalid Queue 后,对核心1 来说看到的 a 和 b 的写入又出现乱序了。

解决办法是继续加内存屏障,核心1 想越过屏障必须清空 Invalid Queue,及时处理了对 a 的无效,然后读取到新的 a 值,如下代码:

int a = 0, b = 0;
// CPU0
void foo() {
    a = 1;
    smp_mb();
    b = 1;
}
// CPU1
void bar() {
    while (b == 0) continue;
    smp_mb(); //继续加内存屏障
    assert(a == 1);
}

这里使用的内存屏障是全屏障,包括读写屏障,过于严格了,会导致性能下降,所以有了细粒度的读屏障和写屏障。

读写屏障分离

分离的写屏障和读屏障的出现,是为了更加精细地控制 Store Buffer 和 Invalid Queue 的顺序。

读屏障不允许其前后的读操作越过屏障
写屏障不允许其前后的写操作越过屏障

int a = 0, b = 0;
// CPU0
void foo() {
    a = 1;
    smp_wmb(); //写屏障
    b = 1;
}
// CPU1
void bar() {
    while (b == 0) continue;
    smp_rmb(); //读屏障
    assert(a == 1);
}

单向屏障

单向屏障 (half-way barrier) 也是一种内存屏障,但它不是以读写来区分的,而是像单行道一样,只允许单向通行,例如 ARM 中的 stlr 和 ldar 指令就是这样。

  • stlr 的全称是 store release register,包括 StoreStore barrier 和 LoadStore barrier(场景少),通常使用 release 语义将寄存器的值写入内存
  • ldar 的全称是 load acquire register,包括 LoadLoad barrier 和 LoadStore barrier,通常使用 acquire 语义从内存中将值加载入寄存器

release 语义的内存屏障只不允许其前面的读写向后越过屏障,挡前不挡后
acquire 语义的内存屏障只不允许其后面的读写向前越过屏障,挡后不挡前

x86-TSO

在这里插入图片描述x86-TSO 有下面几个特点:

  • Store Buffer 被实现为 FIFO 队列,CPU 务必优先读取本地 Store Buffer 中的值(如果有的话),否则去缓存或内存里读取;
  • 因为 Store Buffer 是 FIFO,所以写写不会重排,也就不需要 StoreStore barrier;
  • MFENCE 指令用于清空本地 Store Buffer,并将数据刷到缓存和内存;
  • 某 CPU 执行 lock 前缀的指令时,会去争抢全局锁,拿到锁后其他线程的读取操作会被阻塞,在释放锁之前,会清空该线程的本地的 Store Buffer,这里和 MFENCE 执行逻辑类似;
  • Store Buffer 被写入变量后,除了被其他线程持有锁以外的情况,在任何时刻均有可能写回内存。
    因为没有引入 Invalid Queue,所以不需要 LoadLoad barrier;
  • LoadStore barrier 仅在乱序(out-of-order)处理器上有效,因为等待写指令可以绕过读指令;而 x86-TSO 相对其他平台缓存一致性是最严格的,读操作不会延后,不会使读写重排;
  • 那么最后只有 StoreLoad barrier 是有效的,其他屏障都是no-op。

C++ 内存模型

C++11 原子操作的很多函数都有个 std::memory_order 参数,这个参数就是这里所说的内存模型,对应缓存一致性模型,其作用是对同一时间的读写操作进行排序,一共定义了 6 种类型如下:

memory_order_relaxed:松散内存序,只用来保证对原子对象的操作是原子的,在不需要保证顺序时使用;

memory_order_release:释放操作,在写入某原子对象时,当前线程的任何前面的读写操作都不允许重排到这个操作的后面去,并且当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见;通常与memory_order_acquire 或 memory_order_consume 配对使用;

memory_order_acquire:获得操作,在读取某原子对象时,当前线程的任何后面的读写操作都不允许重排到这个操作的前面去,并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见;

memory_order_consume:同 memory_order_acquire 类似,区别是它仅对依赖于该原子变量操作涉及的对象,比如这个操作发生在原子变量 a 上,而 s = a + b;那 s 依赖于 a,但 b 不依赖于 a;当然这里也有循环依赖的问题,例如:t = s + 1,因为 s 依赖于 a,那 t 其实也是依赖于 a 的;在大多数平台上,这只会影响编译器的优化;不建议使用;

memory_order_acq_rel:获得释放操作,一个读‐修改‐写操作同时具有获得语义和释放语义,即它前后的任何读写操作都不允许重排,并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见,当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见;

memory_order_seq_cst:顺序一致性语义,对于读操作相当于获得,对于写操作相当于释放,对于读‐修改‐写操作相当于获得释放,是所有原子操作的默认内存序,并且会对所有使用此模型的原子操作建立一个全局顺序,保证了多个原子变量的操作在所有线程里观察到的操作顺序相同,当然它是最慢的同步模型。

在不同的 CPU 架构上,这些模型的具体实现方式可能不同,但是 C++11 帮你屏蔽了内部细节,不用考虑内存屏障,只要符合上面的使用规则,就能得到想要的效果。可能有时使用的模型粒度比较大,会损耗性能,当然还是使用各平台底层的内存屏障粒度更准确,效率也会更高。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1693028.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java开发大厂面试第22讲:Redis 是如何保证系统高可用的?它的实现方式有哪些?

高可用是通过设计&#xff0c;减少系统不能提供服务的时间&#xff0c;是分布式系统的基础也是保障系统可靠性的重要手段。而 Redis 作为一款普及率最高的内存型中间件&#xff0c;它的高可用技术也非常的成熟。 我们今天分享的面试题是&#xff0c;Redis 是如何保证系统高可用…

Mac上安装OpenLDAP服务器详细教程(Homebrew安装和自带的ldap)

目录 前言 一、安装 Homebrew&#xff08;如果尚未安装&#xff09; 二、使用 Homebrew 安装 OpenLDAP 三、配置 OpenLDAP 步骤一&#xff1a;更新PATH和环境变量 步骤二&#xff1a;配置slapd.conf 步骤三&#xff1a;初始化和启动 OpenLDAP 服务 1.创建数据库目录 2…

你真的会使用Vue3的onMounted钩子函数吗?Vue3中onMounted的用法详解

目录 一、onMounted的前世今生 1.1、onMounted是什么 1.2、onMounted在vue2中的前身 1.2.1、vue2中的onMounted 1.2.2、Vue2与Vue3的onMounted对比 1.3、vue3中onMounted的用法 1.3.1、基础用法 1.3.2、顺序执行异步操作 1.3.3、并行执行多个异步操作 1.3.4、执行一次…

非等值连接、等值连接、自然连接

目录 一、笛卡尔积 二、三种连接的关系 三、非等值连接 四、等值连接 五、自然连接 一、笛卡尔积 要理解非等值连接、等值连接、自然连接首先要理解笛卡尔积。 学过《离散数学》的应该很熟悉笛卡尔积。 简单来说&#xff0c;就是有两个集合&#xff0c;其中一个集合中的元…

【华三包过】2024年/华三H3C/云计算GB0-713

H3CNE-cloud-云计算-713 想转行 想继续深入 题库覆盖百分百&#xff0c;题库有新版106道新版113道旧版88道 H3C认证云计算工程师&#xff08;H3C Certified Network Engineer for Cloud&#xff0c;简称H3CNE-Cloud&#xff09; 认证定位于全面掌握虚拟化技术原理及相关产品/…

部门来了个测试开发,听说是00后,上来一顿操作给我看蒙了...

公司新来了个同事&#xff0c;听说大学是学的广告专业&#xff0c;因为喜欢IT行业就找了个培训班&#xff0c;后来在一家小公司实习半年&#xff0c;现在跳槽来我们公司。来了之后把现有项目的性能优化了一遍&#xff0c;服务器缩减一半&#xff0c;性能反而提升4倍&#xff01…

Ollama| 搭建本地大模型,最简单的方法!效果直逼GPT

很多人想在本地电脑上搭建一个大模型聊天机器人。总是觉得离自己有点远&#xff0c;尤其是对ai没有了解的童鞋。那么今天我要和你推荐ollama&#xff0c;无论你是否懂开发&#xff0c;哪怕是零基础&#xff0c;只需十分钟&#xff0c;Ollama工具就可以帮助我们在本地电脑上搭建…

【vue-6】监听

一、监听watch 完整示例代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Documen…

回见,那果园

记不得何时开始骑行&#xff0c;何时开始爬山&#xff0c;何时偶遇洛师傅&#xff0c;何时进了那半山腰的果园。 似乎很远&#xff0c;又很近。 昨天打电话给果园的师傅&#xff0c;本意问问杏是否熟了&#xff0c;周末骑行过去、进山聊天顺道吃个新鲜。 洛师傅呵呵的笑…

【vue-1】vue入门—创建一个vue应用

最近在闲暇时间想学习一下前端框架vue&#xff0c;主要参考以下两个学习资料。 官网 快速上手 | Vue.js b站学习视频 2.创建一个Vue3应用_哔哩哔哩_bilibili 一、创建一个vue3应用 <!DOCTYPE html> <html lang"en"> <head><meta charset&q…

KubeKey 安装 K8s

官网教程 在 Linux 上以 All-in-One 模式安装 KubeSphere 步骤 1&#xff1a;准备 Linux 机器 若要以 All-in-One 模式进行安装&#xff0c;您仅需参考以下对机器硬件和操作系统的要求准备一台主机。 硬件推荐配置 操作系统最低配置Ubuntu 16.04, 18.04, 20.04, 22.042 核 …

C++入门:从C语言到C++的过渡(3)

目录 1.内联函数 1.1内联函数的定义 1.2特性 2.auto关键字 2.1auto的简介 2.2注意事项 3.范围for 4.nullptr空指针 1.内联函数 在C语言中&#xff0c;无论使用宏常量还是宏函数都容易出错&#xff0c;而且无法调试。而C为了弥补这一缺陷&#xff0c;引入了内联函数的概…

SpringBoot3.x + JDK21 整合 Mybatis-Plus

前言 SpringBoot3.0 开始最低要求 Java 17&#xff0c;虽然目前最新的版本为 JDK22&#xff0c;但是在官网上看到 JDK23 在今年9月又要发布了&#xff0c;感觉这 JDK 也有点太过于给力了 所以我们选择用目前的 LTS 版本 JDK21 就好了&#xff0c;不用追求最新的 springboot 版…

【大数据】MapReduce JAVA API编程实践及适用场景介绍

目录 1.前言 2.mapreduce编程示例 3.MapReduce适用场景 1.前言 本文是作者大数据系列专栏的其中一篇&#xff0c;前文我们依次聊了大数据的概论、分布式文件系统、分布式数据库、以及计算引擎mapreduce核心概念以及工作原理。 书接上文&#xff0c;本文将会继续聊一下mapr…

未来十年,IT行业的无限可能!

未来十年&#xff0c;IT行业的无限可能&#xff01; &#x1f604;生命不息&#xff0c;写作不止 &#x1f525; 继续踏上学习之路&#xff0c;学之分享笔记 &#x1f44a; 总有一天我也能像各位大佬一样 &#x1f3c6; 博客首页 怒放吧德德 To记录领地 &#x1f31d;分享学…

2024 电工杯(B题)数学建模完整思路+完整代码全解全析

你是否在寻找数学建模比赛的突破点&#xff1f;数学建模进阶思路&#xff01; 作为经验丰富的数学建模团队&#xff0c;我们将为你带来2024电工杯数学建模竞赛&#xff08;B题&#xff09;的全面解析。这个解决方案包不仅包括完整的代码实现&#xff0c;还有详尽的建模过程和解…

短剧APP开发,短剧行业发展下的财富密码

今年以来&#xff0c;短剧市场展现出了繁荣发展的态势&#xff0c;成为了一个风口赛道。 短剧具有不拖沓、时长短、剧情紧凑等优势&#xff0c;顺应了当代人的生活&#xff0c;是当代人的“电子榨菜”。 短剧的快速发展同时也带动了新业态新模式的发展&#xff0c;短剧APP就是…

【Avalonia入门篇1】Avalonia的安装

目录 一、环境安装 1.1 安装.NET 1.2 安装Avalonia模板 1.3 安装VS编译器 1.4 安装Avalonia扩展 二、创建第一个Avalonia项目 一、环境安装 1.1 安装.NET 按照官网提示下载并安装.NET。如下图所示&#xff1a; 安装完成后&#xff0c;打开power shell&#xff0c;输入下…

Vue的学习 —— <Echarts组件库技术应用>

目录 前言 正文 一、ECharts技术简介 二、Vue3集成Echarts 1、安装Echarts 2、引入方式 三、Echarts基础篇 1、图表容器及大小 2、样式 2.1 颜色主题 3、坐标轴 5、数据集 5.1 在series中设置数据集 5.2 在dataset中设置数据集 四、常用图表实操 1、柱状图 2、…

特殊变量笔记2

案例需求 在demo4.sh中循环打印输出所有输入参数, 体验$*与$的区别 实现步骤 编辑demo4.sh脚本文件 # 增加命令: 实现直接输出所有输入后参数 # 增加命令: 使用循环打印输出所有输入参数演示 编辑demo4.sh文件 直接输出所有输入参数, 与循环方式输出所有输入参数(使用双引…