编译器屏障概述

文章目录

  • 1. 前言
  • 2. 编译器内存屏障
    • 2.1 编译器内存访问重排序规则
    • 2.2 编译器屏障的几种形式
      • 2.2.1 显式编译器屏障
      • 2.2.2 隐式编译器屏障
      • 2.2.3 硬件内存屏障充当编译屏障
      • 2.2.4 编程语言内存模型提供的编译屏障
    • 2.3 编译器内存屏障实例
      • 2.3.1 Linux spinlock
  • 3. 结语
  • 4. 参考资料

1. 前言

2. 编译器内存屏障

编译屏障,相对于运行时硬件屏障,它是一种编译时行为,其目的是阻止编译器对内存访问实行程序员不期望的优化行为,如存储访问合并、将循环内的读取行为移动到循环外等等。

2.1 编译器内存访问重排序规则

虽然编译器可以优化代码的存储访问,以得到更好的性能,但编译器开发人员CPU 供应商普遍应遵循的内存访问重排序的一个基本规则,该规则可以表述为:不得修改单线程程序的行为。这条基本规则的意思是,不管是编译时编译器内存访问重排序,还是运行时CPU 内存访问重排序,都要在不改变单线程行为的前提下进行。如有以下代码:

int x, y;void foo(void)
{x = y + 1;y = 0;
}

假定代码通过如下版本 ARM GCC 编译器(这也是本文所有示范代码采用的编译器)进行编译:

$ arm-linux-gnueabi-gcc --version
arm-linux-gnueabi-gcc (Ubuntu/Linaro 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

不启用优化的情形下编译,会生成如下汇编代码(只截取函数 foo() 对应部分):

$ arm-linux-gnueabi-gcc -S foo.c
/* foo.s */....type	foo, %function
foo:@ args = 0, pretend = 0, frame = 0@ frame_needed = 1, uses_anonymous_args = 0@ link register save eliminated.str	fp, [sp, #-4]!add	fp, sp, #0ldr	r3, .L2 // r3 = &yldr	r3, [r3] // r3 = yadd	r3, r3, #1 // r3 = y + 1ldr	r2, .L2+4 // r2 = &xstr	r3, [r2] // (1) x = y + 1ldr	r3, .L2 // r3 = &ymov	r2, #0 // r2 = 0str	r2, [r3] // (2) y = 0nopsub	sp, fp, #0@ sp neededldr	fp, [sp], #4bx	lr
.L3:.align	2
.L2:.word	y.word	x
...

从上面编译器生成的汇编代码代码我们看到,在上面 (1)(2) 处,先 x = y + 1;y = 0; 是符合编程顺序的。再看一下,在(加上 -O2 编译选项)启用编译器优化的情形下,会生成怎样的汇编代码(只截取函数 foo() 对应部分):

$ arm-linux-gnueabi-gcc -O2 -S foo.c
/* foo.s */....type	foo, %function
foo:@ args = 0, pretend = 0, frame = 0@ frame_needed = 0, uses_anonymous_args = 0@ link register save eliminated.mov	r0, #0ldr	r2, .L2 // r2 = &yldr	r1, .L2+4 // r1 = &xldr	r3, [r2] // r3 = ystr	r0, [r2] // (1) x = 0 add	r3, r3, #1 // r3 = y + 1str	r3, [r1] // (2) x = y + 1bx	lr
.L3:.align	2
.L2:.word	y.word	x
...

我们看到,优化后的汇编代码,在 (1)(2) 处,将 x = 0; 的赋值操作,放在了 x = y + 1 的赋值操作之前,这前面的编程顺序不一致,也即编译器出现了对存储访问的重排序。由于 y 的初始值为0,所以 x = 0;x = y + 1 无论哪个在前执行,在单线程执行环境下,都不影响结果,这遵循前面提到的规则。
由于这条规则,编写单线程代码的程序员基本上不会注意到内存重排序。在多线程编程中,它也经常被忽视,因为互斥锁信号量自带编译屏障功能,可以防止在它们周围的内存操作重排序。只有当使用无锁技术时,当内存在线程之间共享而没有任何互斥操作时,可以清楚地观察到内存重排序的效果。编译器的内存访问重排序通常只在启用优化的时候发生。在单处理器系统上,编译屏障已经足够保障对数据的并发访问安全,因为不会出现真正的并行访问,毕竟系统中只有一个处理器。

2.2 编译器屏障的几种形式

2.2.1 显式编译器屏障

以 GCC 编译器为例,来说明显式编译器屏障,其定义如下:

__asm__ volatile("" ::: "memory")

"memory" 阻止将变量缓存到寄存器,强制从内存读取;同时如果访问变量既没有出现在 __asm__ 语句的 input 部分,也没有出现在 __asm__ 语句的 output 部分,则还要加上 __volatile__ 关键字,强制从内存读取。__volatile__ 关键字同时也阻止了编译移动语句(譬如移出循环),指令重排。
编译器(显式)屏障的作用,就是阻止对其前后的内存访问进行重排序。还是以章节 2.1 的代码为例,在其中插入插入一个显式编译器屏障,仍然用 -O2 优化选项进行编译,看是否能阻止对 x = y + 1; 和 y = 0; 的重排序。修改后的代码如下:

int x, y;void foo(void)
{x = y + 1;asm volatile("" ::: "memory"); // 插入一个编译屏障y = 0;
}

再看一下,在(加上 -O2 编译选项)启用编译器优化的情形下,会生成怎样的汇编代码(只截取函数 foo() 对应部分):

$ arm-linux-gnueabi-gcc -O2 -S foo.c
....type	foo, %function
foo:@ args = 0, pretend = 0, frame = 0@ frame_needed = 0, uses_anonymous_args = 0@ link register save eliminated.@ A = B + 1;ldr	r2, .L2 // r2 = &yldr	r1, .L2+4 // r1 = &xldr	r3, [r2] // r3 = yadd	r3, r3, #1 // r3 = y + 1str	r3, [r1] // (1) x = y + 1mov	r3, #0 // r3 = 0str	r3, [r2] // (2) y = 0bx	lr
.L3:.align	2
.L2:.word	y.word	x
...

我们看到,在加入(显式)编译器屏障后,优化后的汇编代码,在 (1)(2) 处,已经按编程顺序执行了对 xy 的存储访问,这是编译屏障起了作用。

2.2.2 隐式编译器屏障

在代码里面的一些 sequence points 如函数调用、对 volatile 变量的访问等,可以作为隐式编译屏障。事实上,大多数函数调用都可充当编译器屏障,无论它们自身是否包含编译器屏障,但这不包括内联函数、使用 pure 属性声明的函数以及使用链接时生成的代码。另外,带编译屏障的函数,哪怕内联了,也作为编译屏障使用。下面来看函数做为隐式编译器屏障的一个例子:

int x, y;void foo(void)
{x = y + 1;y = 0;
}struct foo_data {int bar, bar2;
};void do_something(struct foo_data *foo_data)
{foo_data->bar = 5;foo();foo_data->bar2 = foo_data->bar;
}

开启 -O2 选项进行编译:

$ arm-linux-gnueabi-gcc -O2 -S foo.c

生成如下汇编代码(只截取相关部分):

....type	do_something, %function
do_something:@ args = 0, pretend = 0, frame = 0@ frame_needed = 0, uses_anonymous_args = 0str	lr, [sp, #-4]! // 保存 lr 到堆栈mov	r1, #5 // r1 = 5mov	lr, #0 // lr = 0ldr	r2, .L6 // r2 = &yldr	ip, .L6+4 // ip = &xldr	r3, [r2] // r3 = ystr	r1, [r0, #4] // (1) foo_data->bar2 = 5add	r3, r3, #1 // r3 = y + 1str	r1, [r0] // foo_data->bar = 5 (2)str	lr, [r2] // y = 0str	r3, [ip] // x = y + 1ldr	pc, [sp], #4 // 从 do_something() 函数返回
.L7:.align	2
.L6:.word	y.word	x
...

从汇编代码看到,函数 foo() 已经被内联到函数 do_something() 内,且 在 (1)(2) 处,foo_data->bar = 5;foo_data->bar2 = foo_data->bar; 的顺序和编程顺序正好相反,即产生了编译乱序。为避免这种情况,第 1 种方法是阻止函数 foo() 被内联,第 2 种方法是在函数 foo() 内插入编译屏障。这里只演示第 1 种方法,修改后代码如下:

int x, y;void foo(void) __attribute__((noinline)); // 阻止函数 foo() 被内联void foo(void)
{x = y + 1;y = 0;
}struct foo_data {int bar, bar2;
};void do_something(struct foo_data *foo_data)
{foo_data->bar = 5;foo();foo_data->bar2 = foo_data->bar;
}

开启 -O2 选项进行编译:

$ arm-linux-gnueabi-gcc -O2 -S foo.c

生成如下汇编代码(只截取相关部分):

	.type	do_something, %function
do_something:@ args = 0, pretend = 0, frame = 0@ frame_needed = 0, uses_anonymous_args = 0mov	r3, #5 // r3 = 5push	{r4, lr} // 保存 r4, lr 到堆栈mov	r4, r0 // r4 = foo_datastr	r3, [r0] // (1) foo_data->bar = 5;bl	foo // 调用函数 foo(),函数没有被内联了ldr	r3, [r4] // (2) r3 = foo_data->barstr	r3, [r4, #4] // (3) foo_data->bar2 = foo_data->bar;pop	{r4, pc} // 从堆栈恢复 r4,并从函数返回

(1)(3) 处看到,编译器优化后的代码,仍然保持对 foo_data->barfoo_data->bar2 写操作顺序和代码一致,另外,从 (2) 处看到,对 foo_data->bar2 的赋值前,重新读取了 foo_data->bar 的值,这是合理的,因为我们无法知道,函数 foo() 是否对 foo_data->bar 进行了修改(譬如函数参数 foo_data 指向一个全局变量,而刚好 foo() 修改了它)
除了这些情况之外,对外部函数的调用甚至比编译器障碍更强,因为编译器不知道函数的副作用是什么。编译器必须忘记它对内存所做的任何假设,这些假设可能对该函数可见。仔细想想,这很有道理。在上面的代码片段中,假设 foo() 实现存在于外部库中,编译器如何知道 foo() 不依赖于 foo_data->bar 的值?它如何知道 foo() 不会修改内存中的 foo_data->bar?显然编译器不得而知。因此,为了遵守内存访问重排序的基本规则,它不得围绕对 foo() 这个外部调用对任何内存访问进行重排序。同样,在调用完成后,它必须从内存中加载 foo_data->bar 的新值,而不是假设它仍然等于 5,即使启用了优化。
在许多情况下,编译器指令重新排序是被禁止的,甚至当编译器必须从内存中重新加载某些值时也是如此。这些隐藏的规则构成了人们长期以来一直说 C 中的 valotile 数据类型,在正确编写的多线程代码中通常不是必需的重要原因。

2.2.3 硬件内存屏障充当编译屏障

硬件内存屏障,也可以充当编译屏障。而在单处理器系统上,所有的硬件内存屏障定义都退化为编译屏障。如 Linux 的内存屏障接口单处理器系统上定义如下:

/* include/asm-generic/barrier.h *//** Force strict CPU ordering. And yes, this is required on UP too when we're* talking to devices.** Fall back to compiler barriers if nothing better is provided.*/#ifndef mb
#define mb()	barrier()
#endif#ifndef rmb
#define rmb()	mb()
#endif#ifndef wmb
#define wmb()	mb()
#endif...#ifdef CONFIG_SMP/* 多处理器系统定义 */#else/* 单处理器系统定义 */#ifndef smp_mb
#define smp_mb()	barrier()
#endif#ifndef smp_rmb
#define smp_rmb()	barrier()
#endif#ifndef smp_wmb
#define smp_wmb()	barrier()
#endif#endif	/* CONFIG_SMP */

2.2.4 编程语言内存模型提供的编译屏障

譬如 C++,引入了自己的内存模式,并提供内存屏障接口对存储访问保序。如下面的例子:

int value;
std::atomic<int> is_updated(0);void update_value(int x)
{value = x;// 在这里阻止 value 和 is_updated 存储操作的重排序is_updated.store(1, std::memory_order_release);
}

2.3 编译器内存屏障实例

2.3.1 Linux spinlock

/* kernel/locking/qspinlock.c *//** Per-CPU queue node structures; we can never have more than 4 nested* contexts: task, softirq, hardirq, nmi.** Exactly fits one 64-byte cacheline on a 64-bit architecture.** PV doubles the storage and uses the second cacheline for PV state.*/
/* per-cpu 的数组 mcs_nodes[MAX_NODES] */
static DEFINE_PER_CPU_ALIGNED(struct mcs_spinlock, mcs_nodes[MAX_NODES]);void queued_spin_lock_slowpath(struct qspinlock *lock, u32 val)
{...
queue:node = this_cpu_ptr(&mcs_nodes[0]); // node 指向当前 CPU 的 mcs_nodes[MAX_NODES] 数组idx = node->count++; // count 最大值为 MAX_NODEStail = encode_tail(smp_processor_id(), idx);node += idx; // 使用当前 CPU mcs_nodes[MAX_NODES] 的 mcs_nodes[idx]/** Ensure that we increment the head node->count before initialising* the actual node. If the compiler is kind enough to reorder these* stores, then an IRQ could overwrite our assignments.*/barrier();node->locked = 0;node->next = NULL;pv_init_node(node); // (1) 修改 node 指向的结构体成员变量值...
}

在这里,由于变量 mcs_nodes[MAX_NODES]per-cpu 的,每个 CPU 只会访问自己的变量空间,所以不用考虑多个 CPU 并行、并发访问 mcs_nodes[MAX_NODES] 的情形。在当前上下文,由于 spin_lock() 已经禁用了当前 CPU 上抢占,唯一的并发场景是在中断中也使用同一 spinlock 的情形,所以这里只要保障 node->count++ 操作在对 node 的初始化操作 node->locked = 0; node->next = NULL; pv_init_node(node); 之前完成,那么就不会出现中断中对当前 CPU 数据 mcs_nodes[idx] 的覆写,因为 mcs_nodes[MAX_NODES] 是每 CPU 的数据,所以不会有多个 CPU 对它的并行访问,对它的访问相当于单核场景。所以这里要做的,就是简单的插入一个编译屏障 barrier() 就可以了。如果读者难以能理解这里的场景,那么可以反过来思考:如果对 node 的初始化操作 node->locked = 0;node->next = NULL; pv_init_node(node); 先于 node->count++ 发生,会变成怎样?譬如某个线程刚好执行完了 pv_init_node(node); 修改了 node 的值,在 node->count++ 执行前,中断进来了,也使用和线程相同的 spinlock,然后因为线程中 node->count++ 还没执行,所以中断和线程使用同一个 node,然后修改 node 的值,这时候前面线程对 node 的修改值就会被中断中的写操作覆写。如果 保证 node->count++ 执行在前,那么线程和中断修改的将会是不同的 node ,也就不会出现覆写的情况。

3. 结语

虽然编译乱序为我们带来了很多烦恼,但它只影响无锁编程的场合,因为带锁的场合,锁自身就含有内存屏障语义
在多 CPU 系统下,编译器屏障无法阻止 CPU 带来的存储乱序,这需要硬件内存屏障来保护。
如果我们不是编译器的开发者,或者对使用的编译的各个细节非常熟悉,这时候编译器生成的汇编代码可以指导我们应该怎样、或这哪里插入编译屏障。

4. 参考资料

[1] How can I judge where should I put memory barrier in the code?
[2] GCC-Inline-Assembly-HOWTO
[3] Memory Ordering at Compile Time
[4] Optimization of conditional access to globals: thread-unsafe?
[5] https://www.oracle.com/technetwork/server-storage/solarisstudio/documentation/oss-compiler-barriers-176055.pdf
[6] https://topic.alibabacloud.com/a/understanding-memories-barrier-memory-barrier_8_8_10252214.html
[7] https://yarchive.net/comp/linux/ACCESS_ONCE.html
[8] https://yarchive.net/comp/linux/memory_barriers.html
[9] https://www.quora.com/Can-you-explain-what-a-compiler-barrier-is
[10] https://alibaba-cloud.medium.com/memory-model-and-synchronization-primitive-part-1-memory-barrier-9585e50b4735
[11] https://android.googlesource.com/kernel/msm/+/android-msm-marlin-3.18-nougat-dr1/Documentation/memory-barriers.txt
[12] https://hackmd.io/@VIRqdo35SvekIiH4p76B7g/Hy9JyrBw?type=view
[13] https://developer.arm.com/documentation/den0024/a/Memory-Ordering/Barriers/Use-of-barriers-in-C-code

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

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

相关文章

Minio启动脚本-Windows版

MinIO 是一种高性能、S3 兼容的对象存储。 它专为大规模 AI/ML、数据湖和数据库工作负载而构建,并且它是由软件定义的存储。 不需要购买任何专有硬件,就可以在云上和普通硬件上拥有分布式对象存储。 MinIO拥有开源 GNU AGPL v3 和商业企业许可证的双重许可。 ——摘自…

MySQL之创建高性能的索引(十二)

创建高性能的索引 支持多种过滤条件 这些索引将满足大部分最常见的搜索查询&#xff0c;但是如何为一些生僻的搜索条件(比如has_pictures、eye_color、hair_colr和education)来设计索引呢&#xff1f;这些列的选择性搞&#xff0c;使用也不频繁&#xff0c;可以选择忽略它们&…

云计算和雾计算

雾计算作为传统集中式数据存储系统&#xff08;云&#xff09;和边缘设备之间的中间层。雾扩展了云&#xff0c;使计算和数据存储更接近边缘。雾由多个节点&#xff08;雾节点&#xff09;组成&#xff0c;并创建一个本地网络&#xff0c;使其成为一个去中心化的生态系统——雾…

如何创建一个Angular项目(超简单)

1、安装Node.js&#xff08;官网Node.js下载&#xff09; 2、运行node -v和npm -v两条命令&#xff08;检验是否下载成功Node.js&#xff09; 3、npm i -g cnpm --registryhttps://registry.npmmirror.com&#xff08;用npm安装cnpm&#xff0c;将镜像源设置为国内镜像源&…

ROS2 (python)构造服务端和客户端

要注意&#xff1a;服务端和客户端的名字要统一 什么是线程死锁 A需要等B把装满水的桶放下 才能把水桶提走 B需要等A把水桶提走才能把自己的装满 假设A和B都要站在位置C工作 A&#xff1a;站在工位C将小桶水倒进大桶里 B&#xff1a;从C位置将装满水的大桶拎走 那么假设时刻…

Next前端是什么

Next前端是什么 在前端开发的浩瀚星空中&#xff0c;Next前端如同一颗璀璨的明星&#xff0c;吸引着无数开发者的目光。那么&#xff0c;Next前端究竟是什么呢&#xff1f;它为何能在前端领域掀起如此波澜&#xff1f;接下来&#xff0c;我们将从四个方面、五个方面、六个方面…

制作一个简单HTML旅游网站(HTML+CSS+JS)云南旅游网页设计与实现5个页面

一、&#x1f468;‍&#x1f393;网站题目 旅游&#xff0c;当地特色&#xff0c;历史文化&#xff0c;特色小吃等网站的设计与制作。 二、✍️网站描述 云南旅游主题的网页 一共七个个页面 - 旅游网页使用html css js制作 有banana图 - 页面可以相互跳转 包含表单 三级页面…

Laravel 富文本内容

Laravel 获取富文本的纯文本内容-CSDN博客 Laravel 富文本内容里面的图片添加前缀URL-CSDN博客 Laravel 富文本图片的style样式删除-CSDN博客. 富文本字体font-famly删除 $data preg_replace(/(<[^>])style["\][^"\]*font-family:[^"\]*["\]([^&…

Shopee爆款打造核心运营方法,下一个大卖就是你

课程下载&#xff1a;Shopee爆款打造核心运营方法&#xff0c;下一个大卖就是你-课程网盘链接提取码下载.txt资源-CSDN文库 更多资源下载&#xff1a;关注我。 课程内容&#xff1a; 001.1-爆款产品的选择,mp4 002.2-依据宗教信仰和消费禁忌选品.mp4 003.3-刻画消费者画像…

C++设计模式-桥接模式

运行在VS2022&#xff0c;x86&#xff0c;Debug下。 29. 桥接模式 桥接模式将抽象与实现分离&#xff0c;使二者可以独立地变化。 应用&#xff1a;如在游戏开发中&#xff0c;多个角色和多个武器交叉组合时。可以使用桥接模式&#xff0c;定义角色抽象类&#xff0c;武器抽象…

如何跨渠道分析销售数据 - 7年制造业销售经验小结

如何跨渠道分析销售数据 - 7年制造业销售经验小结&#xff08;1&#xff09; 【前言】 在我过去7年销售工作生涯中&#xff0c;从第一年成为公司销冠后&#xff0c;我当时的确自满的一段时间&#xff0c;认为自己很了不起。但是第一年的销售业绩并没有拿到提成&#xff0c;最…

2024-06-02 问AI: 在大语言模型中,什么是multi agent?

文心一言 在大语言模型中&#xff0c;Multi-Agent&#xff08;多智能体&#xff09;是指由多个具有自主决策和交互能力的智能体&#xff08;Agent&#xff09;组成的系统。这些智能体能够相互协作、竞争或协商&#xff0c;以完成共同或各自的任务。以下是关于Multi-Agent在大语…

爱情的短视频短片:四川京之华锦信息技术公司

爱情的短视频短片&#xff1a;情感瞬间的浓缩与绽放 在数字化时代&#xff0c;短视频以其短小精悍、直击人心的特点&#xff0c;迅速占领了互联网的高地。而在这些纷繁复杂的短视频内容中&#xff0c;关于爱情的短视频短片更是以其独特的魅力&#xff0c;吸引了无数观众的目光…

【U-Net验证】逐元素乘积将特征投射到极高维隐式特征空间的能力

写在前面&#xff1a;本博客仅作记录学习之用&#xff0c;部分图片来自网络&#xff0c;如需使用请注明出处&#xff0c;同时如有侵犯您的权益&#xff0c;请联系删除&#xff01; 文章目录 前言网络结构编码结构解码结构代码 实验实验设置w/o-ReLU的性能比较with-ReLU的性能比…

【Kotlin】简单介绍与使用kotlin

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a;Kotlin ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 特点 变量和常量 数据类型和类型推断 函数 字符串模板 条件表达式 空安全 when 表达式 循环 我的其他博客 前言 Kotlin是…

C语言编程可以拦截弹窗吗:深入探索与解析

C语言编程可以拦截弹窗吗&#xff1a;深入探索与解析 在编程的世界里&#xff0c;C语言以其简洁、高效和接近硬件的特性而著称。然而&#xff0c;当涉及到弹窗拦截这样的任务时&#xff0c;很多人可能会对C语言的能力产生疑问。那么&#xff0c;C语言编程真的可以拦截弹窗吗&a…

适合技术小白学习的项目1840java swing社团管理系统myeclipse开发Mysql数据库CS结构java编程

一、源码特点 java swing社团管理系统 是一套完善的窗体设计系统&#xff0c;对理解SWING java 编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;&#xff0c;系统主要采用C/S模式开发。 应用技术&#xff1a;javamysql 开发工具&#xff1a;Myecl…

Flutter开发效率提升1000%,Flutter Quick教程之对被遮挡的组件进行编辑

1&#xff0c;有些时候&#xff0c;有的widget会被其他widget所遮挡&#xff0c;那么&#xff0c;我们如何选中被遮挡的Widget?如下面这张图。上面是一个Text&#xff0c;外面包裹着一个Container&#xff0c;这时候点击事件会被Text所拦截&#xff0c;那么&#xff0c;如何选…

零基础画师文创运营变现课,从0基础到入门一步步提升(46节课)

课程下载&#xff1a;零基础画师文创运营变现课&#xff0c;从0基础到入门一步步提升&#xff08;46节课&#xff09;-课程网盘链接提取码下载.txt资源-CSDN文库 更多资源下载&#xff1a;关注我。 课程内容&#xff1a; 01 我的二十五年,mp4 02 古法裁剪与新古法裁剪,mp4 …

重塑电商科技版图:从传统架构迈向DDD的华丽蜕变之路

关注微信公众号 “程序员小胖” 每日技术干货&#xff0c;第一时间送达&#xff01; 引言 随着电子商务行业的蓬勃发展&#xff0c;传统的电商系统架构面临着诸多挑战&#xff0c;如扩展性不足、维护成本高、响应市场变化慢等。领域驱动设计&#xff08;Domain-Driven Design&a…