第一章:鹏哥C语言Go语言混合系统性能调优导论
在现代高性能服务架构中,C语言与Go语言的混合系统日益普遍:C承担底层硬件交互、零拷贝网络收发与实时计算密集型模块,Go则负责高并发API网关、协程编排与配置热加载。二者通过CGO桥接或共享内存IPC通信,但天然存在内存模型差异、调度语义冲突与性能观测盲区——这使得传统单语言调优方法失效。
混合系统典型性能瓶颈特征
- C模块中未对齐的结构体导致CPU缓存行浪费(false sharing)
- Go goroutine 频繁调用CGO函数引发P线程阻塞与GMP调度退化
- 共享内存区域缺乏原子操作保护,引发竞态与数据撕裂
- Profiling工具链割裂:pprof无法穿透CGO栈帧,perf亦难关联Go运行时符号
关键调优入口点识别
使用以下命令快速定位混合热点:
# 同时采集Go原生profile与内核级采样(需提前编译启用-gcflags="-l"避免内联干扰)
go tool pprof -http=:8080 ./app &
sudo perf record -e cycles,instructions,cache-misses -g -p $(pgrep app) -- sleep 30
sudo perf script | stackcollapse-perf.pl | flamegraph.pl > hybrid-flame.svg
该流程生成的火焰图中,若出现 runtime.cgocall → C.function_name → libc 的长链,即表明CGO调用开销已成瓶颈。
内存协同优化基础原则
| 维度 | C侧建议 | Go侧对应实践 |
|---|---|---|
| 内存分配 | 使用mmap(MAP_HUGETLB)分配大页内存 | 通过unsafe.Slice()直接映射C内存块 |
| 生命周期管理 | 禁用free(),交由Go runtime GC管理 | 在finalizer中调用C.free() |
| 缓存友好性 | 结构体按64字节对齐,敏感字段分组 | 使用//go:pack注释提示编译器对齐 |
真实调优始于对边界行为的敬畏:每次CGO调用隐含一次用户态/内核态切换与栈拷贝;每个跨语言指针传递都绕过Go的垃圾回收可达性分析。唯有将C与Go视为同一内存宇宙中的共生体,而非黑盒接口,方能释放混合系统的真正吞吐潜力。
第二章:C语言底层性能剖析与内核级观测实践
2.1 基于perf的全链路采样与火焰图深度标注实战
全链路性能洞察需穿透用户请求→应用→内核→硬件的完整调用栈。perf 是 Linux 原生低开销采样核心工具,配合 --call-graph dwarf 可捕获带符号信息的深度调用链。
采样命令与关键参数
sudo perf record -e cycles,instructions,cache-misses \
--call-graph dwarf,8192 \
-g -p $(pgrep -f "myapp") -- sleep 30
-e指定多事件联合采样,实现 CPI(cycles per instruction)与缓存失效关联分析;--call-graph dwarf,8192启用 DWARF 解析(支持内联函数、优化后栈帧),8192 字节栈深度保障长链路完整性;-g启用传统 frame-pointer 回退机制,增强兼容性。
火焰图生成与标注增强
使用 perf script | stackcollapse-perf.pl | flamegraph.pl --title "CPU Flame Graph (DWARF)" > flame.svg 生成基础火焰图后,通过 --color=java 或自定义 --hash + 注释映射表注入业务语义标签(如 auth::validate_token)。
| 标注维度 | 实现方式 | 示例值 |
|---|---|---|
| 调用路径标签 | --comm 过滤 + --symfs 加载调试符号 |
nginx:worker |
| 时延区间色阶 | 自定义 flamegraph.pl 颜色映射 |
>5ms → 深红 |
| 模块归属标识 | --pid + /proc/[pid]/cmdline 解析 |
payment-service:v2.4 |
graph TD
A[HTTP Request] --> B[App Layer JIT Frame]
B --> C[libc malloc]
C --> D[Kernel mm/mmap.c]
D --> E[Hardware TLB Miss]
2.2 内存布局分析与cache line对齐优化实操
现代CPU访问内存时,以 64字节 cache line 为最小单位加载数据。若结构体跨cache line分布,一次读取将触发两次缓存填充,显著降低性能。
结构体对齐前后的内存占用对比
| 字段 | 类型 | 偏移(未对齐) | 偏移(alignas(64)) |
|---|---|---|---|
id |
uint32_t |
0 | 0 |
flags |
uint8_t |
4 | 4 |
padding |
— | — | 55 bytes(显式填充) |
| 总大小 | — | 12 | 64 |
对齐优化代码示例
struct alignas(64) HotData {
uint32_t id;
uint8_t flags;
// 编译器自动填充至64字节边界
};
static_assert(sizeof(HotData) == 64, "Must occupy exactly one cache line");
逻辑分析:
alignas(64)强制结构体起始地址为64字节对齐,确保单次cache line加载即可覆盖全部字段;static_assert在编译期验证对齐效果,避免运行时隐式错误。参数64对应主流x86-64平台L1/L2 cache line大小。
性能影响路径
graph TD
A[结构体字段分散] --> B[跨cache line访问]
B --> C[额外缓存行填充]
C --> D[LLC miss率↑ / 延迟↑]
E[alignas 64] --> F[单cache line覆盖]
F --> G[原子读写加速 / false sharing↓]
2.3 系统调用开销量化与strace+eBPF联合验证
为什么需要双工具协同?
单靠 strace 仅能捕获用户态系统调用路径,但无法观测内核路径延迟、上下文切换开销或内核函数内部耗时;eBPF 则可精准插桩内核函数入口/出口,弥补可观测性盲区。
典型验证流程
# 同时运行:strace 记录调用序列 + eBPF 脚本统计内核执行时长
sudo strace -e trace=write,read -T -p $(pgrep nginx) 2>&1 | head -5
此命令输出含
-T参数,显示每次系统调用的挂钟耗时(秒),但该值包含调度延迟、中断等待等噪声,非纯内核执行时间。
eBPF 侧关键采样点
// bpf_program.c:在 sys_write 入口/出口打点
SEC("tracepoint/syscalls/sys_enter_write")
int handle_enter(struct trace_event_raw_sys_enter *ctx) {
u64 ts = bpf_ktime_get_ns();
bpf_map_update_elem(&start_time_map, &pid, &ts, BPF_ANY);
return 0;
}
使用
bpf_ktime_get_ns()获取纳秒级时间戳;start_time_map是哈希表,以 PID 为键存储进入时间,供出口函数计算差值。
量化对比结果(单位:μs)
| 工具 | write() 平均耗时 | 覆盖维度 |
|---|---|---|
| strace -T | 18.7 | 用户可见总延迟 |
| eBPF tracepoint | 3.2 | 纯内核执行阶段 |
| 差值 | 15.5 | 调度/上下文开销 |
协同验证价值
- ✅ 揭示“系统调用慢”的真实归因(是内核慢?还是调度卡顿?)
- ✅ 为优化提供分层依据:若 eBPF 值正常而 strace 值高 → 优先排查 CPU 抢占或 cgroup 限频
2.4 GCC编译器级性能调优(-O3/-march/-flto)与汇编反查
现代C/C++高性能场景中,编译器是第一道性能杠杆。-O3启用激进优化(如向量化、函数内联、循环展开),但可能增大代码体积;-march=native让编译器生成当前CPU专属指令集(如AVX2、BMI2),显著提升吞吐;-flto(Link-Time Optimization)跨目标文件进行全局视图优化,使内联和死代码消除更彻底。
gcc -O3 -march=native -flto -o app main.o utils.o
此命令启用三级协同优化:
-O3驱动前端优化决策,-march=native释放硬件潜能,-flto在链接阶段重做IR级分析与优化,避免传统编译单元隔离导致的优化盲区。
反查汇编验证效果:
objdump -d app | grep -A5 "compute_loop"
输出可确认是否生成
vmulps(AVX浮点乘)、vpaddd(向量加)等指令,验证-march与-O3协同生效。
| 选项 | 作用域 | 风险提示 |
|---|---|---|
-O3 |
编译单元内 | 可能增加栈深度或延迟 |
-march=native |
指令集特化 | 二进制不可移植至老CPU |
-flto |
全程序链接期 | 增加编译内存与时间开销 |
graph TD A[源码.c] –> B[预处理/词法分析] B –> C[前端优化 -O3] C –> D[生成GIMPLE IR] D –> E[跨模块LTO合并] E –> F[后端代码生成 -march] F –> G[最终可执行文件]
2.5 C语言多线程锁竞争建模与futex状态追踪
数据同步机制
C语言中,pthread_mutex_t 底层常依赖 futex(2) 系统调用实现轻量级等待/唤醒。其核心在于原子状态机:(空闲)、1(已加锁)、>1(存在等待者)。
futex状态迁移模型
// 原子比较并交换:若*uaddr == val,则设为 newval,返回0;否则返回EBUSY
int futex(int *uaddr, int op, int val,
const struct timespec *timeout, int *uaddr2, int val3);
uaddr:用户态共享整型锁变量地址(如 mutex->data.lock)op:常用FUTEX_WAIT(阻塞)或FUTEX_WAKE(唤醒)val:期望的当前值,防止 ABA 竞态
竞争建模关键参数
| 状态变量 | 含义 | 典型取值 |
|---|---|---|
futex_word |
用户态锁状态字 | 0 / 1 / 256 |
waiters |
内核等待队列长度 | 由内核维护 |
spurious_wake |
伪唤醒发生概率 |
graph TD
A[线程尝试加锁] --> B{CAS成功?}
B -->|是| C[进入临界区]
B -->|否| D[futex(FUTEX_WAIT)]
D --> E[内核挂起线程]
F[释放锁] --> G[futex(FUTEX_WAKE)]
G --> H[唤醒一个等待者]
第三章:Go运行时深度解构与调度器性能干预
3.1 Goroutine调度轨迹可视化与GMP状态机动态抓取
Go 运行时通过 runtime/trace 和 debug/gcroots 提供底层调度事件采集能力,核心在于捕获 Goroutine 状态跃迁(如 _Grunnable → _Grunning → _Gwaiting)及 P 的绑定/解绑时机。
数据采集入口
启用追踪需在程序启动时调用:
import "runtime/trace"
// ...
f, _ := os.Create("trace.out")
trace.Start(f)
defer trace.Stop()
此代码启动全局事件采样器,以微秒级精度记录
G、M、P状态变更、阻塞点(如 channel send/receive)、系统调用进出等。trace.Stop()触发元数据序列化,生成可被go tool trace解析的二进制流。
GMP状态机关键跃迁表
| G 状态 | 触发条件 | 关联 P 操作 |
|---|---|---|
_Grunnable |
go f() 创建后入就绪队列 |
绑定至空闲 P |
_Grunning |
P 从本地队列摘取并执行 | 已绑定且独占 |
_Gsyscall |
进入阻塞系统调用(如 read) |
P 被 M 释放 |
调度路径可视化流程
graph TD
A[New Goroutine] --> B[G.status = _Grunnable]
B --> C{P.localRunq 是否有空位?}
C -->|是| D[P 执行 runqget]
C -->|否| E[G 入 global runq]
D --> F[G.status = _Grunning]
F --> G[执行中遇 channel 阻塞]
G --> H[G.status = _Gwaiting]
3.2 GC停顿归因分析与pprof+runtime/trace双模调试
Go 程序中突增的 GC 停顿常源于内存分配模式异常或对象生命周期失控。单靠 go tool pprof 的堆采样难以捕捉瞬时 STW 行为,需结合 runtime/trace 的精细时序能力。
双模采集命令
# 启动带 trace 和 pprof 支持的服务
GODEBUG=gctrace=1 ./myapp &
# 采集 30s 追踪数据
curl -s "http://localhost:6060/debug/trace?seconds=30" -o trace.out
# 同步抓取堆/协程/GC 概览
curl -s "http://localhost:6060/debug/pprof/goroutine?debug=2" > goroutines.txt
该命令组合确保在相同负载窗口下获取 GC 触发点、STW 时长及协程阻塞上下文,避免时间偏移导致归因失准。
关键诊断维度对比
| 维度 | pprof 优势 |
runtime/trace 优势 |
|---|---|---|
| 时间精度 | 秒级采样 | 微秒级事件标记(如 GCSTW, GCMarkAssist) |
| 对象生命周期 | 堆快照(allocs/inuse) | GC 阶段状态流转与暂停切片 |
| 协同分析能力 | 支持火焰图调用链 | 可叠加 goroutine 执行轨迹 |
GC停顿根因定位流程
graph TD
A[trace.out] --> B{分析 GC 阶段耗时}
B --> C[识别异常长 STW:>10ms]
C --> D[关联该时刻 pprof heap profile]
D --> E[定位高分配率类型:如 []byte, map[string]*T]
E --> F[检查是否未复用 buffer 或缓存泄漏]
3.3 Go内存分配器(mcache/mcentral/mheap)热点定位与调优
Go运行时内存分配器采用三层结构:mcache(每P私有缓存)、mcentral(全局中心缓存)、mheap(堆页管理器)。高频小对象分配易在mcache与mcentral交界处引发锁竞争或跨P迁移。
热点识别方法
- 使用
go tool trace分析runtime.alloc事件分布 - 观察
GODEBUG=gctrace=1输出中scvg和sweep频次 - 通过
pprof -alloc_space定位高分配率类型
典型优化策略
- 复用对象:
sync.Pool缓存临时结构体实例 - 控制对象大小:避免跨越 size class 边界(如 96B → 112B 将降级至更大class)
- 减少逃逸:
go build -gcflags="-m"检查栈分配可行性
var bufPool = sync.Pool{
New: func() interface{} {
b := make([]byte, 1024)
return &b // 返回指针以避免复制开销
},
}
此池按需创建1KB字节切片指针,规避每次
make([]byte, 1024)触发mcache分配及潜在mcentral锁争用;New函数仅在Get无可用对象时调用,降低mheap页申请频率。
| 组件 | 并发模型 | 关键瓶颈 |
|---|---|---|
| mcache | 无锁(per-P) | 容量耗尽后需加锁访问mcentral |
| mcentral | 中心锁 | 多P同时 refill 时竞争 |
| mheap | 全局锁(部分) | 大对象分配/页回收扫描 |
graph TD
A[goroutine 分配 32B 对象] --> B[mcache.sizeclass[4]]
B -- 缓存空 --> C[mcentral.sizeclass[4].lock]
C -- refill 20 个span --> D[mheap.allocSpan]
D --> C --> B --> A
第四章:C/Go混合调用场景下的协同性能治理
4.1 CGO调用栈穿透分析与cgo_check=0风险规避实践
CGO调用栈穿透指 Go 运行时无法追踪 C 函数内部的 goroutine 栈帧,导致 panic 传播中断、runtime.Stack() 截断、pprof 采样失真。
栈穿透典型表现
- Go panic 在 C 函数中终止,不触发
defer链 GODEBUG=cgocall=1日志显示cgocall后无后续栈帧
cgo_check=0 的双刃剑
# 禁用 CGO 检查(危险!)
CGO_ENABLED=1 GOFLAGS="-gcflags=all=-cgo_check=0" go build
逻辑分析:
-cgo_check=0关闭 Go 编译器对 C 指针逃逸、内存生命周期的静态校验。参数-gcflags=all=表示作用于所有包;cgo_check=0绕过*C.xxx转换合法性检查,易引发 use-after-free 或栈变量地址误传。
安全替代方案对比
| 方案 | 安全性 | 调试支持 | 适用场景 |
|---|---|---|---|
cgo_check=2(默认) |
✅ 强校验 | ✅ 完整栈 | 开发/测试 |
手动 //export + C.free 显式管理 |
✅ | ✅ | 生产高频调用 |
runtime.SetFinalizer + unsafe.Pointer 封装 |
⚠️ 需谨慎 | ❌ 栈不可见 | 遗留 C 库适配 |
graph TD
A[Go 函数调用 C] --> B[cgo_check=2 校验]
B -->|通过| C[生成安全 wrapper]
B -->|失败| D[编译报错:invalid C pointer]
C --> E[完整调用栈可追溯]
4.2 共享内存通信(mmap+atomic)替代CGO的零拷贝优化
传统 CGO 调用在 Go 与 C 间频繁传递大数据时,需多次内存拷贝与 GC 干预,成为性能瓶颈。共享内存 + 原子操作可彻底规避数据复制。
核心优势对比
| 方式 | 内存拷贝 | 跨语言边界 | 同步开销 | GC 压力 |
|---|---|---|---|---|
| CGO | ✅ 多次 | ✅ 高成本 | 依赖锁/chan | ✅ 显著 |
| mmap + atomic | ❌ 零拷贝 | ✅ 同一地址空间 | ✅ lock-free | ❌ 无 |
初始化共享页(Go 端)
fd, _ := unix.Open("/dev/shm/mybuf", unix.O_CREAT|unix.O_RDWR, 0600)
unix.Ftruncate(fd, 4096)
addr, _ := unix.Mmap(fd, 0, 4096, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
// addr 是跨进程可见的 []byte 底层指针;4096=页对齐大小,PROT_WRITE允写,MAP_SHARED确保C端可见
数据同步机制
使用 atomic.Uint32 在共享内存首部标记就绪状态,C 端轮询该原子值即可实现无锁等待:
// C端伪代码:读取原子标志(假设偏移0处为uint32)
while (atomic_load_explicit((_Atomic uint32*)shm_addr, memory_order_acquire) == 0) {
sched_yield(); // 轻量等待
}
graph TD
A[Go写入数据] –> B[atomic.StoreUint32(flag, 1)]
B –> C[C端atomic.LoadUint32检测]
C –> D[直接访问同一物理页数据]
4.3 eBPF内核探针在混合栈回溯中的定制化注入(含源码级hook)
混合栈回溯需同时捕获用户态符号与内核态调用链,传统perf采样存在上下文割裂问题。eBPF通过kprobe/uprobe协同注入,实现零拷贝跨域栈帧拼接。
核心注入点选择
__x64_sys_read(内核入口)libc中read@plt(用户态桩函数)libpthread中__pthread_start(线程创建锚点)
源码级hook示例(BPF C)
SEC("kprobe/__x64_sys_read")
int trace_read_entry(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
// 保存寄存器状态用于后续栈展开
bpf_map_update_elem(&entry_map, &pid, ctx, BPF_ANY);
return 0;
}
逻辑说明:
pt_regs包含完整CPU寄存器快照,entry_map为BPF_MAP_TYPE_HASH,键为pid_tgid,值为struct pt_regs*指针。该映射供后续trace_read_exit读取并触发栈遍历。
混合栈重建流程
graph TD
A[kprobe entry] --> B[保存regs到map]
C[uprobe read@plt] --> D[读取用户栈帧]
B --> E[exit时关联双栈]
D --> E
E --> F[生成统一stack_id]
| 组件 | 作用 |
|---|---|
bpf_get_stack() |
内核栈采样(需CONFIG_BPF_KPROBE_OVERRIDE) |
bpf_get_user_stack() |
用户栈解析(依赖vma映射) |
stack_map |
存储归一化栈哈希 |
4.4 bpftrace监控模板体系构建:覆盖C函数入口、Go GC事件、syscall延迟三维度
统一模板设计原则
采用 probe 抽象层解耦目标语言语义:C 函数用 uprobe,Go GC 事件通过 tracepoint:gc:gc_start 捕获,syscall 延迟则基于 kprobe:sys_enter_* + kretprobe:sys_exit_* 配对计时。
核心模板示例(syscall延迟)
# syscall-latency.bt
kprobe:sys_enter_read { $start[tid] = nsecs; }
kretprobe:sys_exit_read /$start[tid]/ {
@latency = hist(nsecs - $start[tid]);
delete $start[tid];
}
逻辑分析:$start[tid] 以线程 ID 为键存储进入时间;nsecs 提供纳秒级精度;hist() 自动构建对数分布直方图,避免手动分桶。参数 $start 为临时标量映射,生命周期限于单次 probe 触发。
三维度能力对比
| 维度 | 探针类型 | 触发条件 | 典型延迟误差 |
|---|---|---|---|
| C 函数入口 | uprobe | 用户态符号地址命中 | |
| Go GC 事件 | tracepoint | 内核 GC 子系统显式抛出 | ≈ 0ns(零拷贝) |
| Syscall 延迟 | kprobe/kretprobe | 内核态上下文切换点 | ~200ns |
数据聚合流程
graph TD
A[Probe 触发] --> B{语言上下文}
B -->|C ABI| C[uprobe + USDT]
B -->|Go runtime| D[tracepoint:gc:*]
B -->|Kernel interface| E[kprobe/syscall]
C & D & E --> F[统一@hist/@count聚合]
F --> G[实时输出/导出JSON]
第五章:训练营结业项目与工业级调优方法论总结
结业项目:电商评论情感分析实时服务系统
学员团队基于真实脱敏电商数据(含127万条用户评论、5类细粒度情感标签)构建端到端服务。模型采用BERT-base微调+CRF序列标注联合架构,在验证集上达到92.3% F1-score;服务层使用FastAPI封装,通过ONNX Runtime加速推理,P99延迟压降至86ms(QPS=1200)。关键挑战在于处理“反讽表达”(如“好评!差评!”),最终通过引入对抗训练(FGM)与领域词典增强(融合京东/拼多多商品术语表)将该类样本准确率从61.4%提升至85.7%。
工业级模型压缩实践路径
| 优化阶段 | 技术手段 | 模型体积变化 | 推理耗时下降 | 精度损失(F1) |
|---|---|---|---|---|
| 基线 | PyTorch FP32 | 421 MB | — | — |
| 量化 | Dynamic Quantization | 108 MB | 3.2× | -0.4% |
| 剪枝 | Magnitude-based Pruning (30%) | 76 MB | 4.8× | -0.9% |
| 蒸馏 | TinyBERT蒸馏 | 52 MB | 7.1× | -0.3% |
生产环境A/B测试验证框架
部署双通道服务:主通道(全量模型)与灰度通道(压缩模型)并行接收10%线上流量。通过Prometheus采集指标:model_latency_p99{service="sentiment", variant="full"} 与 model_latency_p99{service="sentiment", variant="tiny"},结合Grafana看板实时对比。发现压缩模型在长文本(>512字符)场景下因截断导致精度波动,遂引入动态分块处理逻辑——将超长评论切分为语义连贯子句,分别预测后加权融合,使长文本F1回升至91.8%。
持续监控告警策略
# 实时数据漂移检测(KS检验)
def check_drift(batch_features: np.ndarray, ref_distribution: np.ndarray):
ks_stat, p_value = kstest(batch_features.flatten(), ref_distribution.flatten())
if p_value < 0.01 and ks_stat > 0.15:
alert_slack("SENTIMENT_DRIFT_CRITICAL", f"KS={ks_stat:.3f}, p={p_value:.3f}")
trigger_retrain_pipeline()
多维度性能瓶颈归因流程
flowchart TD
A[服务延迟升高] --> B{CPU使用率 > 90%?}
B -->|Yes| C[定位高负载Worker进程]
B -->|No| D{GPU显存占用突增?}
D -->|Yes| E[检查ONNX输入张量shape异常]
D -->|No| F[分析Python GIL争用:启用uvloop+asyncpg]
C --> G[启用cProfile采样:发现JSON序列化占时47%]
G --> H[替换ujson库,延迟降低210ms]
模型版本回滚SOP
当新版本上线后30分钟内出现error_rate > 5%或latency_p99 > 150ms连续5个采样周期,自动触发回滚:① K8s滚动更新至前一stable镜像;② 将当前模型权重快照存入MinIO的/rollback/sentiment-v2.3-20240521/路径;③ 向DataOps平台推送回滚事件,关联Jira故障单自动生成根因分析模板。
真实故障复盘:春节大促期间OOM事件
2月10日20:15,服务Pod因OOMKilled重启。根因分析显示:用户突发上传含12000字符的长评论(含大量emoji与URL),原始截断逻辑未清理HTML实体编码,导致tokenized长度达5892,超出ONNX模型最大序列长度约束。修复方案为在预处理层强制注入<TRUNC>标记,并对URL统一替换为[URL]占位符,使极端case token数稳定在≤512。
模型服务安全加固要点
- 输入层强制UTF-8编码校验,拦截含BOM头或混合编码请求
- 使用
huggingface/tokenizers的add_special_tokens=False禁用潜在恶意token注入 - Prometheus指标暴露
model_input_length_histogram直方图,设置rate(model_input_length_count{le="512"}[5m]) / rate(model_input_length_count[5m]) < 0.95触发告警
成果交付物清单
- 完整Dockerfile(含CUDA 11.8 + TensorRT 8.6编译环境)
- Terraform脚本(AWS EKS集群自动扩缩容配置)
- 模型卡(Model Card)PDF文档(含偏差分析、跨地域测试结果)
- 数据血缘图谱(Apache Atlas集成,追踪从Kafka Topic到特征Store的全链路)
