首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
1
「今日早报」 2026年6月7日, 农历四月廿二, 星期日
2
「今日早报」 2026年6月6日, 农历四月廿一, 星期六
3
「今日早报」 2026年5月16日, 农历三月卅十, 星期六
4
「今日早报」 2026年5月15日, 农历三月廿九, 星期五
5
「今日早报」 2026年5月14日, 农历三月廿八, 星期四
沙漠渔
把過去的累積,善用到當下
累计撰写
2,789
篇文章
累计创建
385
个标签
累计收到
997
条评论
栏目
首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
搜索
标签搜索
代理服务
winsw
override
VMware
api
popai
拉取镜像
人工智能
copilot
chatgpt
openai
coze
objdump
ldd
日志
版本
latest
批处理
bat
节能模式
iwconfig
排序
du
设计模式
hostname
面板
cockpit
版本不兼容
npm
统计
烟花
新春
leveldb
Java heap space
堆内存
harbor
utf8mb4
网络聚合
IPV6
nmtui
测速
带宽
千兆
路由器
nmcli
nmlci
orangepi
motd
中文乱码
webdav
香橙派
代码折叠
享元模式
单例模式
解锁
锁定
无法安装
Xshell
并发编程
ScheduledThreadPoolExecutor
ThreadPoolExecutor
线程池
Fock-Join
并发
ExecutorService
nextcloud
alist
panic
hung task
时间戳
ping
tail
dd
嵌入式
点灯
mount
共享
NFS
curl
全屏
ChannelOption
C++
comparator
桥头堡
开源
varchar
char
StringBuilder
StringBuffer
String
命令行工具
网络配置
netsh
建议
专家
离谱
2022
设备管理器
虚拟网卡
环回适配器
安全
攻击
CC
DDOS
跳槽
过年
年假
春节
2023
优站计划
鼻塞
咳嗽
大号流感
阳
新冠病毒
循环冗余校验
CRC-16/XMODEM
crc校验
stream
DMZ主机
域名解析
CDN
七牛云
DDNS
类加载器
双亲委派
加载机制
删除
搜索
变化时间
修改时间
访问时间
响应异常
超时
jsoup
用法详解
压缩命令
打包压缩
zip
优缺点
打包
数据压缩
密码
sudoers
索引量
权重
瞬间
魔鬼洞
沙漠鱼
沙漠渔
面试
引用传递
值传递
即时编译器
机器码
JIT
旗舰版
ISO
原版镜像
win7
参数
配置文件
8小时
时间错误
报告
coverity
jetbrains
idea
谷歌翻译
rest
403
ERR_UNKNOWN_URL_SCHEME
DevTools
优先级
location
FTP
挂载
curlftpfs
提示
自定义参数
springboot
配置
死锁
超级密码
桥接
联通光猫
Dockerfile
构建
命令行
eclipse
光猫
路由模式
桥接模式
bash-4.2
sudo
oracle帐号
JDK下载
百度收录
定时发布
哨兵模式
内存管理
JVM
登录
免密
不一致
服务器时间
BIO,NIO,AIO
ByteBuffer
FileWriter
BufferedOutputStream
流水线
pipeline
环境变量
重启
任务消失
jenkins
卸载
snap
20.04
Ubuntu
容器
大数据传输
InputStream
学习笔记
CSS
最佳实践
DOM操作
原型链
原型
No such file or Directory
SSH配置
SSH免密
tar
命令
网站优化
取消快照
百度快照
全局配置
修改密码
控制台
gitlab
解码器
Netty
文件传输
sz
rz
Jar
打铁花
羊毛沟
容器日志
docker
规范
博客
rejected
Gerrit
wireshark
代理
Go
http-server
nodejs
package-lock.json
996加班
删除标签
钟薛高
60s新闻接口
每日新闻接口
百度福利
自动回复
localstorage
QQ机器人
redis
近乎
客户端
JuiceSSH
乔恩
加菲猫
新浪邮箱
joe
公告
父亲节
图标
每日新闻
51la
服务器
腾讯云
插件
罗永浩
编码
avatar
小虎墩大英雄
端午劫
端午节
中石化
中国石化
儿童节
win10
激活
shell
金门大桥
旧金山
产品经理
免疫力
熬夜
云旅游
云游
招聘
海尔
halo
校友会
创新
哈工大
鸿蒙
王成录
标题
主题
北京
疫情
域名
沙漠渔溏
方法声明
jpa
表情包
emoji
JavaAgent
姓名测试
姓名打分
起名
百度
收录
SEO
group by
distinct
去重
JDBC
validationQuery
兄弟元素
点击事件
$event
9600
传输延时
波特率
串口
站点统计
站长工具
cnzz
生命周期
refs
vue
replaceAll
replace
JavaScript
软件
FRP
内网外入
内网穿透
学习
解题
leetcode
云计算
中国医药
神思电子
股票
Java
报错
数据块
关键词
风筝
清明节
注解
spring
clang-format
格式化
反向代理
nginx
索引
linux
vim
数据库
仓库管理
git
压缩
winrar
mysql
测试
markdown
目 录
CONTENT
以下是
网络聚合
相关的文章
2024-01-19
【译】.NET 7 中的性能改进(三)
原文 | Stephen Toub 翻译 | 郑子铭 PGO 我在我的 .NET 6 性能改进一文中写了关于配置文件引导优化 (profile-guided optimization) (PGO) 的文章,但我将在此处再次介绍它,因为它已经看到了 .NET 7 的大量改进。 PGO 已经存在了很长时间,有多种语言和编译器。基本思想是你编译你的应用程序,要求编译器将检测注入应用程序以跟踪各种有趣的信息。然后你让你的应用程序通过它的步伐,运行各种常见的场景,使该仪器“描述”应用程序执行时发生的事情,然后保存结果。然后重新编译应用程序,将这些检测结果反馈给编译器,并允许它根据预期的使用方式优化应用程序。这种 PGO 方法被称为“静态 PGO”,因为所有信息都是在实际部署之前收集的,这是 .NET 多年来一直以各种形式进行的事情。不过,从我的角度来看,.NET 中真正有趣的开发是“动态 PGO”,它是在 .NET 6 中引入的,但默认情况下是关闭的。 动态 PGO 利用分层编译。我注意到 JIT 检测第 0 层代码以跟踪方法被调用的次数,或者在循环的情况下,循环执行了多少次。它也可以将它用于其他事情。例如,它可以准确跟踪哪些具体类型被用作接口分派的目标,然后在第 1 层专门化代码以期望最常见的类型(这称为“保护去虚拟化 (guarded devirtualization)”或 GDV)。你可以在这个小例子中看到这一点。将 DOTNET_TieredPGO 环境变量设置为 1,然后在 .NET 7 上运行: class Program { static void Main() { IPrinter printer = new Printer(); for (int i = 0; ; i++) { DoWork(printer, i); } } static void DoWork(IPrinter printer, int i) { printer.PrintIfTrue(i == int.MaxValue); } interface IPrinter { void PrintIfTrue(bool condition); } class Printer : IPrinter { public void PrintIfTrue(bool condition) { if (condition) Console.WriteLine("Print!"); } } } DoWork 的第 0 层代码最终看起来像这样: G_M000_IG01: ;; offset=0000H 55 push rbp 4883EC30 sub rsp, 48 488D6C2430 lea rbp, [rsp+30H] 33C0 xor eax, eax 488945F8 mov qword ptr [rbp-08H], rax 488945F0 mov qword ptr [rbp-10H], rax 48894D10 mov gword ptr [rbp+10H], rcx 895518 mov dword ptr [rbp+18H], edx G_M000_IG02: ;; offset=001BH FF059F220F00 inc dword ptr [(reloc 0x7ffc3f1b2ea0)] 488B4D10 mov rcx, gword ptr [rbp+10H] 48894DF8 mov gword ptr [rbp-08H], rcx 488B4DF8 mov rcx, gword ptr [rbp-08H] 48BAA82E1B3FFC7F0000 mov rdx, 0x7FFC3F1B2EA8 E8B47EC55F call CORINFO_HELP_CLASSPROFILE32 488B4DF8 mov rcx, gword ptr [rbp-08H] 48894DF0 mov gword ptr [rbp-10H], rcx 488B4DF0 mov rcx, gword ptr [rbp-10H] 33D2 xor edx, edx 817D18FFFFFF7F cmp dword ptr [rbp+18H], 0x7FFFFFFF 0F94C2 sete dl 49BB0800F13EFC7F0000 mov r11, 0x7FFC3EF10008 41FF13 call [r11]IPrinter:PrintIfTrue(bool):this 90 nop G_M000_IG03: ;; offset=0062H 4883C430 add rsp, 48 5D pop rbp C3 ret 而最值得注意的是,你可以看到调用[r11]IPrinter:PrintIfTrue(bool):这个做接口调度。但是,再看一下为第一层生成的代码。我们仍然看到调用[r11]IPrinter:PrintIfTrue(bool):this,但我们也看到了这个。 G_M000_IG02: ;; offset=0020H 48B9982D1B3FFC7F0000 mov rcx, 0x7FFC3F1B2D98 48390F cmp qword ptr [rdi], rcx 7521 jne SHORT G_M000_IG05 81FEFFFFFF7F cmp esi, 0x7FFFFFFF 7404 je SHORT G_M000_IG04 G_M000_IG03: ;; offset=0037H FFC6 inc esi EBE5 jmp SHORT G_M000_IG02 G_M000_IG04: ;; offset=003BH 48B9D820801A24020000 mov rcx, 0x2241A8020D8 488B09 mov rcx, gword ptr [rcx] FF1572CD0D00 call [Console:WriteLine(String)] EBE7 jmp SHORT G_M000_IG03 第一块是检查IPrinter的具体类型(存储在rdi中)并与Printer的已知类型(0x7FFC3F1B2D98)进行比较。如果它们不一样,它就跳到它在未优化版本中做的同样的接口调度。但如果它们相同,它就会直接跳到Printer.PrintIfTrue的内联版本(你可以看到这个方法中对Console:WriteLine的调用)。因此,普通情况(本例中唯一的情况)是超级有效的,代价是一个单一的比较和分支。 这一切都存在于.NET 6中,那么为什么我们现在要谈论它?有几件事得到了改善。首先,由于dotnet/runtime#61453这样的改进,PGO现在可以与OSR一起工作。这是一个大问题,因为这意味着做这种接口调度的热的长期运行的方法(这相当普遍)可以得到这些类型的去虚拟化/精简优化。第二,虽然PGO目前不是默认启用的,但我们已经让它更容易打开了。在dotnet/runtime#71438和dotnet/sdk#26350之间,现在可以简单地将 true 放入你的.csproj中。 csproj,它的效果和你在每次调用应用程序之前设置DOTNET_TieredPGO=1一样,启用动态PGO(注意,它不会禁止使用R2R图像,所以如果你希望整个核心库也采用动态PGO,你还需要设置DOTNET_ReadyToRun=0)。然而,第三,是动态PGO已经学会了如何检测和优化额外的东西。 PGO已经知道如何对虚拟调度进行检测。现在在.NET 7中,在很大程度上要感谢dotnet/runtime#68703,它也可以为委托做这件事(至少是对实例方法的委托)。考虑一下这个简单的控制台应用程序。 using System.Runtime.CompilerServices; class Program { static int[] s_values = Enumerable.Range(0, 1_000).ToArray(); static void Main() { for (int i = 0; i < 1_000_000; i++) Sum(s_values, i => i * 42); } [MethodImpl(MethodImplOptions.NoInlining)] static int Sum(int[] values, Func<int, int> func) { int sum = 0; foreach (int value in values) sum += func(value); return sum; } } 在没有启用PGO的情况下,我得到的优化汇编是这样的。 ; Assembly listing for method Program:Sum(ref,Func`2):int ; Emitting BLENDED_CODE for X64 CPU with AVX - Windows ; Tier-1 compilation ; optimized code ; rsp based frame ; partially interruptible ; No PGO data G_M000_IG01: ;; offset=0000H 4156 push r14 57 push rdi 56 push rsi 55 push rbp 53 push rbx 4883EC20 sub rsp, 32 488BF2 mov rsi, rdx G_M000_IG02: ;; offset=000DH 33FF xor edi, edi 488BD9 mov rbx, rcx 33ED xor ebp, ebp 448B7308 mov r14d, dword ptr [rbx+08H] 4585F6 test r14d, r14d 7E16 jle SHORT G_M000_IG04 G_M000_IG03: ;; offset=001DH 8BD5 mov edx, ebp 8B549310 mov edx, dword ptr [rbx+4*rdx+10H] 488B4E08 mov rcx, gword ptr [rsi+08H] FF5618 call [rsi+18H]Func`2:Invoke(int):int:this 03F8 add edi, eax FFC5 inc ebp 443BF5 cmp r14d, ebp 7FEA jg SHORT G_M000_IG03 G_M000_IG04: ;; offset=0033H 8BC7 mov eax, edi G_M000_IG05: ;; offset=0035H 4883C420 add rsp, 32 5B pop rbx 5D pop rbp 5E pop rsi 5F pop rdi 415E pop r14 C3 ret ; Total bytes of code 64 注意其中调用[rsi+18H]Func`2:Invoke(int):int:this来调用委托。现在启用了PGO。 ; Assembly listing for method Program:Sum(ref,Func`2):int ; Emitting BLENDED_CODE for X64 CPU with AVX - Windows ; Tier-1 compilation ; optimized code ; optimized using profile data ; rsp based frame ; fully interruptible ; with Dynamic PGO: edge weights are valid, and fgCalledCount is 5628 ; 0 inlinees with PGO data; 1 single block inlinees; 0 inlinees without PGO data G_M000_IG01: ;; offset=0000H 4157 push r15 4156 push r14 57 push rdi 56 push rsi 55 push rbp 53 push rbx 4883EC28 sub rsp, 40 488BF2 mov rsi, rdx G_M000_IG02: ;; offset=000FH 33FF xor edi, edi 488BD9 mov rbx, rcx 33ED xor ebp, ebp 448B7308 mov r14d, dword ptr [rbx+08H] 4585F6 test r14d, r14d 7E27 jle SHORT G_M000_IG05 G_M000_IG03: ;; offset=001FH 8BC5 mov eax, ebp 8B548310 mov edx, dword ptr [rbx+4*rax+10H] 4C8B4618 mov r8, qword ptr [rsi+18H] 48B8A0C2CF3CFC7F0000 mov rax, 0x7FFC3CCFC2A0 4C3BC0 cmp r8, rax 751D jne SHORT G_M000_IG07 446BFA2A imul r15d, edx, 42 G_M000_IG04: ;; offset=003CH 4103FF add edi, r15d FFC5 inc ebp 443BF5 cmp r14d, ebp 7FD9 jg SHORT G_M000_IG03 G_M000_IG05: ;; offset=0046H 8BC7 mov eax, edi G_M000_IG06: ;; offset=0048H 4883C428 add rsp, 40 5B pop rbx 5D pop rbp 5E pop rsi 5F pop rdi 415E pop r14 415F pop r15 C3 ret G_M000_IG07: ;; offset=0055H 488B4E08 mov rcx, gword ptr [rsi+08H] 41FFD0 call r8 448BF8 mov r15d, eax EBDB jmp SHORT G_M000_IG04 我选择了i => i * 42中的42常数,以使其在汇编中容易看到,果然,它就在那里。 G_M000_IG03: ;; offset=001FH 8BC5 mov eax, ebp 8B548310 mov edx, dword ptr [rbx+4*rax+10H] 4C8B4618 mov r8, qword ptr [rsi+18H] 48B8A0C2CF3CFC7F0000 mov rax, 0x7FFC3CCFC2A0 4C3BC0 cmp r8, rax 751D jne SHORT G_M000_IG07 446BFA2A imul r15d, edx, 42 这是从委托中加载目标地址到r8,并加载预期目标的地址到rax。如果它们相同,它就简单地执行内联操作(imul r15d, edx, 42),否则就跳转到G_M000_IG07,调用r8的函数。如果我们把它作为一个基准运行,其效果是显而易见的。 static int[] s_values = Enumerable.Range(0, 1_000).ToArray(); [Benchmark] public int DelegatePGO() => Sum(s_values, i => i * 42); static int Sum(int[] values, Func<int, int>? func) { int sum = 0; foreach (int value in values) { sum += func(value); } return sum; } 在禁用PGO的情况下,我们在.NET 6和.NET 7中得到了相同的性能吞吐量。 方法 运行时 平均值 比率 DelegatePGO .NET 6.0 1.665 us 1.00 DelegatePGO .NET 7.0 1.659 us 1.00 但当我们启用动态PGO(DOTNET_TieredPGO=1)时,情况发生了变化。.NET 6的速度提高了~14%,但.NET 7的速度提高了~3倍! 方法 运行时 平均值 比率 DelegatePGO .NET 6.0 1,427.7 ns 1.00 DelegatePGO .NET 7.0 539.0 ns 0.38 dotnet/runtime#70377是动态PGO的另一个有价值的改进,它使PGO能够很好地发挥循环克隆和不变量提升的作用。为了更好地理解这一点,简要地说说这些是什么。循环克隆 (Loop cloning) 是JIT采用的一种机制,以避免循环的快速路径中的各种开销。考虑一下本例中的Test方法。 using System.Runtime.CompilerServices; class Program { static void Main() { int[] array = new int[10_000_000]; for (int i = 0; i < 1_000_000; i++) { Test(array); } } [MethodImpl(MethodImplOptions.NoInlining)] private static bool Test(int[] array) { for (int i = 0; i < 0x12345; i++) { if (array[i] == 42) { return true; } } return false; } } JIT不知道传入的数组是否有足够的长度,以至于在循环中对数组[i]的所有访问都在边界内,因此它需要为每次访问注入边界检查。虽然简单地在前面进行长度检查,并在长度不够的情况下提前抛出一个异常是很好的,但这样做也会改变行为(设想该方法在进行时向数组中写入数据,或者以其他方式改变一些共享状态)。相反,JIT采用了 "循环克隆"。它从本质上重写了这个测试方法,使之更像这样。 if (array is not null && array.Length >= 0x12345) { for (int i = 0; i < 0x12345; i++) { if (array[i] == 42) // no bounds checks emitted for this access :-) { return true; } } } else { for (int i = 0; i < 0x12345; i++) { if (array[i] == 42) // bounds checks emitted for this access :-( { return true; } } } return false; 这样一来,以一些代码重复为代价,我们得到了没有边界检查的快速循环,而只需支付慢速路径中的边界检查。你可以在生成的程序集中看到这一点(如果你还不明白,DOTNET_JitDisasm是.NET 7中我最喜欢的功能之一)。 ; Assembly listing for method Program:Test(ref):bool ; Emitting BLENDED_CODE for X64 CPU with AVX - Windows ; Tier-1 compilation ; optimized code ; rsp based frame ; fully interruptible ; No PGO data G_M000_IG01: ;; offset=0000H 4883EC28 sub rsp, 40 G_M000_IG02: ;; offset=0004H 33C0 xor eax, eax 4885C9 test rcx, rcx 7429 je SHORT G_M000_IG05 81790845230100 cmp dword ptr [rcx+08H], 0x12345 7C20 jl SHORT G_M000_IG05 0F1F40000F1F840000000000 align [12 bytes for IG03] G_M000_IG03: ;; offset=0020H 8BD0 mov edx, eax 837C91102A cmp dword ptr [rcx+4*rdx+10H], 42 7429 je SHORT G_M000_IG08 FFC0 inc eax 3D45230100 cmp eax, 0x12345 7CEE jl SHORT G_M000_IG03 G_M000_IG04: ;; offset=0032H EB17 jmp SHORT G_M000_IG06 G_M000_IG05: ;; offset=0034H 3B4108 cmp eax, dword ptr [rcx+08H] 7323 jae SHORT G_M000_IG10 8BD0 mov edx, eax 837C91102A cmp dword ptr [rcx+4*rdx+10H], 42 7410 je SHORT G_M000_IG08 FFC0 inc eax 3D45230100 cmp eax, 0x12345 7CE9 jl SHORT G_M000_IG05 G_M000_IG06: ;; offset=004BH 33C0 xor eax, eax G_M000_IG07: ;; offset=004DH 4883C428 add rsp, 40 C3 ret G_M000_IG08: ;; offset=0052H B801000000 mov eax, 1 G_M000_IG09: ;; offset=0057H 4883C428 add rsp, 40 C3 ret G_M000_IG10: ;; offset=005CH E81FA0C15F call CORINFO_HELP_RNGCHKFAIL CC int3 ; Total bytes of code 98 G_M000_IG02部分正在进行空值检查和长度检查,如果任何一项失败,则跳转到G_M000_IG05块。如果两者都成功了,它就会执行循环(G_M000_IG03块)而不进行边界检查。 G_M000_IG03: ;; offset=0020H 8BD0 mov edx, eax 837C91102A cmp dword ptr [rcx+4*rdx+10H], 42 7429 je SHORT G_M000_IG08 FFC0 inc eax 3D45230100 cmp eax, 0x12345 7CEE jl SHORT G_M000_IG03 边界检查只显示在慢速路径块中。 G_M000_IG05: ;; offset=0034H 3B4108 cmp eax, dword ptr [rcx+08H] 7323 jae SHORT G_M000_IG10 8BD0 mov edx, eax 837C91102A cmp dword ptr [rcx+4*rdx+10H], 42 7410 je SHORT G_M000_IG08 FFC0 inc eax 3D45230100 cmp eax, 0x12345 7CE9 jl SHORT G_M000_IG05 这就是 "循环克隆"。那么,"不变量提升 (invariant hoisting) "呢?提升是指把某个东西从循环中拉到循环之前,而不变量是不会改变的东西。因此,不变量提升是指把某个东西从循环中拉到循环之前,以避免在循环的每个迭代中重新计算一个不会改变的答案。实际上,前面的例子已经展示了不变量提升,即边界检查被移到了循环之前,而不是在循环中,但一个更具体的例子是这样的。 [MethodImpl(MethodImplOptions.NoInlining)] private static bool Test(int[] array) { for (int i = 0; i < 0x12345; i++) { if (array[i] == array.Length - 42) { return true; } } return false; } 注意,array.Length - 42的值在循环的每次迭代中都不会改变,所以它对循环迭代是 "不变的",可以被抬出来,生成的代码就是这样做的。 G_M000_IG02: ;; offset=0004H 33D2 xor edx, edx 4885C9 test rcx, rcx 742A je SHORT G_M000_IG05 448B4108 mov r8d, dword ptr [rcx+08H] 4181F845230100 cmp r8d, 0x12345 7C1D jl SHORT G_M000_IG05 4183C0D6 add r8d, -42 0F1F4000 align [4 bytes for IG03] G_M000_IG03: ;; offset=0020H 8BC2 mov eax, edx 4439448110 cmp dword ptr [rcx+4*rax+10H], r8d 7433 je SHORT G_M000_IG08 FFC2 inc edx 81FA45230100 cmp edx, 0x12345 7CED jl SHORT G_M000_IG03 这里我们再次看到数组被测试为空(test rcx, rcx),数组的长度被检查(mov r8d, dword ptr [rcx+08H] then cmp r8d, 0x12345),但是在r8d中有数组的长度,然后我们看到这个前期块从长度中减去42(add r8d, -42),这是在我们继续进入G_M000_IG03块的快速路径循环前。这使得额外的操作集不在循环中,从而避免了每次迭代重新计算数值的开销。 好的,那么这如何适用于动态PGO呢?请记住,对于PGO能够做到的界面/虚拟调度的规避,它是通过进行类型检查,看使用的类型是否是最常见的类型;如果是,它就使用直接调用该类型方法的快速路径(这样做的话,该调用有可能被内联),如果不是,它就回到正常的界面/虚拟调度。这种检查可以不受循环的影响。因此,当一个方法被分层,PGO启动时,类型检查现在可以从循环中提升出来,使得处理普通情况更加便宜。考虑一下我们原来的例子的这个变化。 using System.Runtime.CompilerServices; class Program { static void Main() { IPrinter printer = new BlankPrinter(); while (true) { DoWork(printer); } } [MethodImpl(MethodImplOptions.NoInlining)] static void DoWork(IPrinter printer) { for (int j = 0; j < 123; j++) { printer.Print(j); } } interface IPrinter { void Print(int i); } class BlankPrinter : IPrinter { public void Print(int i) { Console.Write(""); } } } 当我们看一下在启用动态PGO的情况下为其生成的优化程序集时,我们看到了这个。 ; Assembly listing for method Program:DoWork(IPrinter) ; Emitting BLENDED_CODE for X64 CPU with AVX - Windows ; Tier-1 compilation ; optimized code ; optimized using profile data ; rsp based frame ; partially interruptible ; with Dynamic PGO: edge weights are invalid, and fgCalledCount is 12187 ; 0 inlinees with PGO data; 1 single block inlinees; 0 inlinees without PGO data G_M000_IG01: ;; offset=0000H 57 push rdi 56 push rsi 4883EC28 sub rsp, 40 488BF1 mov rsi, rcx G_M000_IG02: ;; offset=0009H 33FF xor edi, edi 4885F6 test rsi, rsi 742B je SHORT G_M000_IG05 48B9982DD43CFC7F0000 mov rcx, 0x7FFC3CD42D98 48390E cmp qword ptr [rsi], rcx 751C jne SHORT G_M000_IG05 G_M000_IG03: ;; offset=001FH 48B9282040F948020000 mov rcx, 0x248F9402028 488B09 mov rcx, gword ptr [rcx] FF1526A80D00 call [Console:Write(String)] FFC7 inc edi 83FF7B cmp edi, 123 7CE6 jl SHORT G_M000_IG03 G_M000_IG04: ;; offset=0039H EB29 jmp SHORT G_M000_IG07 G_M000_IG05: ;; offset=003BH 48B9982DD43CFC7F0000 mov rcx, 0x7FFC3CD42D98 48390E cmp qword ptr [rsi], rcx 7521 jne SHORT G_M000_IG08 48B9282040F948020000 mov rcx, 0x248F9402028 488B09 mov rcx, gword ptr [rcx] FF15FBA70D00 call [Console:Write(String)] G_M000_IG06: ;; offset=005DH FFC7 inc edi 83FF7B cmp edi, 123 7CD7 jl SHORT G_M000_IG05 G_M000_IG07: ;; offset=0064H 4883C428 add rsp, 40 5E pop rsi 5F pop rdi C3 ret G_M000_IG08: ;; offset=006BH 488BCE mov rcx, rsi 8BD7 mov edx, edi 49BB1000AA3CFC7F0000 mov r11, 0x7FFC3CAA0010 41FF13 call [r11]IPrinter:Print(int):this EBDE jmp SHORT G_M000_IG06 ; Total bytes of code 127 我们可以在G_M000_IG02块中看到,它正在对IPrinter实例进行类型检查,如果检查失败就跳到G_M000_IG05(mov rcx, 0x7FFC3CD42D98 then cmp qword ptr [rsi], rcx then jne SHORT G_M000_IG05),否则就跳到G_M000_IG03,这是一个紧密的快速路径循环,内联BlankPrinter.Print,看不到任何类型检查。 有趣的是,这样的改进也会带来自己的挑战。PGO导致了类型检查数量的大幅增加,因为专门针对某一特定类型的调用站点需要与该类型进行比较。然而,普通的子表达式消除 (common subexpression elimination)(CSE)在历史上并不适用这种类型的句柄(CSE是一种编译器优化,通过计算一次结果,然后存储起来供以后使用,而不是每次都重新计算,来消除重复的表达式)。dotnet/runtime#70580通过对这种常量句柄启用CSE来解决这个问题。例如,考虑这个方法。 [Benchmark] [Arguments("", "", "", "")] public bool AllAreStrings(object o1, object o2, object o3, object o4) => o1 is string && o2 is string && o3 is string && o4 is string; 在.NET 6上,JIT产生了这个汇编代码: ; Program.AllAreStrings(System.Object, System.Object, System.Object, System.Object) test rdx,rdx je short M00_L01 mov rax,offset MT_System.String cmp [rdx],rax jne short M00_L01 test r8,r8 je short M00_L01 mov rax,offset MT_System.String cmp [r8],rax jne short M00_L01 test r9,r9 je short M00_L01 mov rax,offset MT_System.String cmp [r9],rax jne short M00_L01 mov rax,[rsp+28] test rax,rax je short M00_L00 mov rdx,offset MT_System.String cmp [rax],rdx je short M00_L00 xor eax,eax M00_L00: test rax,rax setne al movzx eax,al ret M00_L01: xor eax,eax ret ; Total bytes of code 100 请注意,C#对字符串有四个测试,而汇编代码中的mov rax,offset MT_System.String有四个加载。现在在.NET 7上,加载只执行一次。 ; Program.AllAreStrings(System.Object, System.Object, System.Object, System.Object) test rdx,rdx je short M00_L01 mov rax,offset MT_System.String cmp [rdx],rax jne short M00_L01 test r8,r8 je short M00_L01 cmp [r8],rax jne short M00_L01 test r9,r9 je short M00_L01 cmp [r9],rax jne short M00_L01 mov rdx,[rsp+28] test rdx,rdx je short M00_L00 cmp [rdx],rax je short M00_L00 xor edx,edx M00_L00: xor eax,eax test rdx,rdx setne al ret M00_L01: xor eax,eax ret ; Total bytes of code 69 原文链接 Performance Improvements in .NET 7 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。 如有任何疑问,请与我联系 (
[email protected]
)
2024-01-19
244
0
0
网络聚合
2024-01-19
飞桨paddlespech 语音唤醒初探
PaddleSpeech提供了MDTC模型(paper: The NPU System for the 2020 Personalized Voice Trigger Challenge)在Hey Snips数据集上的语音唤醒(KWS)的实现。这篇论文是用空洞时间卷积网络(dilated temporal convolution network, DTCN)的方法来做的,曾获the 2020 personalized voice trigger challenge (PVTC2020)的第二名,可见这个方案是比较优秀的。想看看到底是怎么做的,于是我对其做了一番初探。 1,模型理解 论文是用空洞时间卷积网络(DTCN)的方法来实现的。为了减少参数量,用了depthwise & pointwise 一维卷积。一维卷积以及BatchNormal、relu等组成1个DTCNBlock, 4个DTCNBlock组成一个DTCNStack。实现的模型跟论文里的有一些差异。论文里的模型具体见论文,实现的模型框图见下图: 模型有PreProcess、DTCNStack(3个, DTCN:空洞时间卷积网络)、FCN(全连接网络)、sigmoid这些模块。PreProcess是做前处理,主要是由3个一维卷积(1个depthwise和两个pointwise)组成。每个DTCNStack由4个DTCNBlock组成,DTCNBlock跟preprocess模块相似,唯一的区别是多了残差模块(图中画红线的)。 这个模型的参数个数不到37K,见下图: 参数个数是比较少的,相对论文里的也少了不少。刚开始我不太相信,后来我对网络中的模型每层都算了参数个数,的确是这么多。想了一下,对比paper里的模型,参数变少主要有两点:一是少了一些模块,二是FCN由linear替代(linear替代FCN会少不少参数)。 模型用的特征是80维的mel-filter bank,即每帧的特征是一个80维的数据。把一个utterance的这些帧的特征作为模型的输入,输出是每一帧的后验概率,如果有一帧的后验概率大于threshold,就认为这一utterance是关键词,从而唤醒设备。举例来说,一个utterance有158帧,模型的输入就是158*80的矩阵(158是帧数,80是特征的维度),输出是158*1的矩阵,即158个后验概率。假设threshold设为0.8,这158个后验概率中只要有一个达到0.8,这个utterance就认为是关键词。 2,环境搭建 PaddleSpeech相关的文档里讲了如何搭建环境(Ubuntu下的),这里简述一下: 1)创建conda环境以及激活这个conda环境等: conda create --name paddletry python=3.7 conda activate paddletry 2)安装 paddelpaddle (paddlespeech 是基于paddelpaddle的) pip install paddlepaddle 3)clone 以及编译paddlespeech 代码 git clone https://github.com/PaddlePaddle/PaddleSpeech.git pip install . 3,数据集准备 数据集用的是sonos公司的”hey snips”。我几天内用三个不同的邮箱去注册申请,均没给下载链接,难道是跟目前在科技领域紧张的中美关系有关?后来联系到了这篇paper的作者, 他愿意分享数据集。在此谢谢他,真是个热心人!他用百度网盘分享了两次数据集,下载后均是tar包解压出错,估计是传输过程中出了问题。在走投无路的情况下尝试去修复坏的tar包。找到了tar包修复工具gzrt,运气不错,能修复大部分,关键是定义train/dev/test集的json文件能修复出来。如果自己写json文件太耗时耗力了。Json中一个wav文件数据格式大致如下: { "duration": 4.86, "worker_id": "0007cc59899fa13a8e0af4ed4b8046c6", "audio_file_path": "audio_files/41dac4fb-3e69-4fd0-a8fc-9590d30e84b4.wav", "id": "41dac4fb-3e69-4fd0-a8fc-9590d30e84b4", "is_hotword": 0 }, 数据集中原有wav文件96396个,修复了81401个。写python把在json中出现的但是audio_files目录中没有的去掉,形成新的json文件。原始的以及新的数据集中train/dev/test wav数如下: 从上表可以看出新的数据集在train/dev/test上基本都是原先的84%左右。 4,训练和评估 在PaddleSpeech/examples/hey_snips/kws0下做训练。训练前要把这个目录下conf/mdtc.yaml里的数据集的路径改成自己放数据集的地方。由于我用CPU训练,相应的命令就是./run.sh conf/mdtc.yaml 。 训练50个epoch(默认配置)后,在验证集下的准确率为99.79%(见下图),还是不错的,就没再训练下去。 评估出的DET图如下: Paddlespeech也提供了KWS推理命令: paddlespeech kws。需要研究一下这个命令是怎么用的,看相关代码。--input 后面既可以是一个具体的wav文件(这时只能评估一个文件),也可以是一个txt文件,把要评估的文件名都写在里面,具体格式如下图: --ckpt_path是模型的路径,--config是设置配置文件,也就是mdtc.yaml。因为要对整个测试集做评估,所以--input要写成txt的形式。Hey Snips数据集wav文件都在audio_files目录下,需要写脚本把测试集的wav文件取出来放在一个目录下(我的是heytest), 还要写脚本把这次测试文件的文件名以及路径写到上图所示的txt文件里。同时还要在paddlespeech 里加些代码看推理出的值是否跟期望值一致,做些统计。把这些都弄好后就开始做运行了,具体命令如下图: 最终测试集下的结果,见下图: 共19442个文件,跟期望一致的(图中correct的)是19410个,准确率为99.84%。与验证集下的大体相当。
2024-01-19
198
0
0
网络聚合
2024-01-19
ASP.NET Core如何知道一个请求执行了哪些中间件?
第一步,添加Nuget包引用 需要添加两个Nuget包分别是:Microsoft.AspNetCore.MiddlewareAnalysis和Microsoft.Extensions.DiagnosticAdapter,前者是分析记录中间件核心代码实现后者是用来接收日志输出的,由于是用的DiagnosticSource方式记录日志,所以需要使用DiagnosticListener对象的SubscribeWithAdapter方法来订阅。 第二步,实现一个分析诊断适配器 这个适配器是为了方便我们把从DiagnosticSource接收到的日志对象输出到控制台,具体代码实现如下 public class AnalysisDiagnosticAdapter { private readonly ILogger<AnalysisDiagnosticAdapter> _logger; public AnalysisDiagnosticAdapter(ILogger<AnalysisDiagnosticAdapter> logger) { _logger = logger; } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareStarting")] public void OnMiddlewareStarting(HttpContext httpContext, string name, Guid instance, long timestamp) { _logger.LogInformation($"中间件-启动: '{name}'; Request Path: '{httpContext.Request.Path}'"); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException")] public void OnMiddlewareException(Exception exception, HttpContext httpContext, string name, Guid instance, long timestamp, long duration) { _logger.LogInformation($"中间件-异常: '{name}'; '{exception.Message}'"); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareFinished")] public void OnMiddlewareFinished(HttpContext httpContext, string name, Guid instance, long timestamp, long duration) { _logger.LogInformation($"中间件-结束: 耗时[{duration/10000}] '{name}'; Status: '{httpContext.Response.StatusCode}'"); } } 第三步,注册相关服务来启用分析中间件的功能 注册中间件分析服务 var builder = WebApplication.CreateBuilder(args); builder.Services.AddMiddlewareAnalysis(); 订阅我们的分析诊断适配器 var listener = app.Services.GetRequiredService<DiagnosticListener>(); var observer = ActivatorUtilities.CreateInstance<AnalysisDiagnosticAdapter>(app.Services); using var disposable = listener.SubscribeWithAdapter(observer); 这样基本就完成了分析记录中间件的功能,启动程序看看效果 日志已经成功的输出到我们的控制台了,不过才四个中间件,应该不止这么少的,再在注册中间件分析服务哪里添加一句代码 var builder = WebApplication.CreateBuilder(args); // 新增的下面这句代码 builder.Services.Insert(0, ServiceDescriptor.Transient<IStartupFilter, AnalysisStartupFilter>()); builder.Services.AddMiddlewareAnalysis(); 现在再来看看效果,发现变成8个中间件了多了四个 在Release模式编译后,运行发现中间件的执行效率非常高,几乎不占用时间 异常记录这里就不放图了,有兴趣的朋友自己去试试。 简单三步就可以知道一个请求到底执行了哪些中间件还是挺方便的。想知道实现原理可以去看看Microsoft.AspNetCore.MiddlewareAnalysis这个库,一共才四个文件看起来不费事。
2024-01-19
208
0
0
网络聚合
2024-01-19
P/Invoke之C#调用动态链接库DLL
本编所涉及到的工具以及框架: 1、Visual Studio 2022 2、.net 6.0 P/Invok是什么? P/Invoke全称为Platform Invoke(平台调用),其实际上就是一种函数调用机制,通过P/Invoke就可以实现调用非托管Dll中的函数。 在开始之前,我们首先需要了解C#中有关托管与非托管的区别 托管(Collocation),即在程序运行时会自动释放内存; 非托管,即在程序运行时不会自动释放内存。 废话不多说,直接实操 第一步: 打开VS2022,新建一个C#控制台应用 右击解决方案,添加一个新建项,新建一个"动态链接库(DLL)",新建完之后需要右击当前项目--> 属性 --> C/C++ --> 预编译头 --> 选择"不使用编译头" 在新建的DLL中我们新建一个头文件,用于编写我们的方法定义,然后再次新建一个C++文件,后缀以.c 结尾 第二步: 在我们DLL中的头文件(Native.h)中定义相关的Test方法,具体代码如下: #pragma once // 定义一些宏 #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN #endif #define CallingConvention _cdecl // 判断用户是否有输入,从而定义区分使用dllimport还是dllexport #ifdef DLL_IMPORT #define HEAD EXTERN __declspec(dllimport) #else #define HEAD EXTERN __declspec(dllexport) #endif HEAD int CallingConvention Sum(int a, int b); 之后需要去实现头文件中的方法,在Native.c中实现,具体实现如下: #include "Native.h" // 导入头部文件 #include "stdio.h" HEAD int Add(int a, int b) { return a+b; } 在这些步骤做完后,可以尝试生成解决方案,检查是否报错,没有报错之后,将进入项目文件中,检查是否生成DLL (../x64/Debug) 第三步: 在这里之后,就可以在C#中去尝试调用刚刚所声明的方法,以便验证是否调用DLL成功,其具体实现如下: using System.Runtime.InteropServices; class Program { [DllImport(@"C:\My_project\C#_Call_C\CSharp_P_Invoke_Dll\x64\Debug\NativeDll.dll")] public static extern int Add(int a, int b); public static void Main(string[] args) { int sum = Add(23, 45); Console.WriteLine(sum); Console.ReadKey(); } } 运行结果为:68,证明我们成功调用了DLL动态链库 C#中通过P/Invoke调用DLL动态链库的流程 通过上述一个简单的例子,我们大致了解到了在C#中通过P/Invoke调用DLL动态链库的流程,接下我们将对C#中的代码块做一些改动,便于维护 在改动中我们将用到NativeLibrary类中的一个方法,用于设置回调,解析从程序集进行的本机库导入,并实现通过设置DLL的相对路径进行加载,其方法如下: public static void SetDllImportResolver (System.Reflection.Assembly assembly, System.Runtime.InteropServices.DllImportResolver resolver); 在使用这个方法前,先查看一下其参数 a、assembly: 主要是获取包含当前正在执行的代码的程序集(不过多讲解) b、resolber: 此参数是我们要注重实现的,我们可以通过查看他的元代码,发现其实现的是一个委托,因此我们对其进行实现。 原始方法如下: public delegate IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath); 实现resolver方法: const string NativeLib = "NativeDll.dll"; static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64","Release", "NativeDll.dll"); // 此处为Dll的路径 //Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } 该方法主要是用于区分在加载DLL时不一定只能是设置绝对路径,也可以使用相对路径对其加载,本区域代码是通过使用委托去实现加载相对路径对其DLL加载,这样做的好处是,便于以后需要更改DLL的路径时,只需要在这个方法中对其相对路径进行修改即可。 更新C#中的代码,其代码如下: using System.Reflection; using System.Runtime.InteropServices; class Program { const string NativeLib = "NativeDll.dll"; [DllImport(NativeLib)] public static extern int Add(int a, int b); static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64","Release", "NativeDll.dll"); Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } public static void Main(string[] args) { NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); int sum = Add(23, 45); Console.WriteLine(sum); Console.ReadKey(); } } 最后重新编译,检查其是否能顺利编译通过,最终我们的到的结果为:68 至此,我们就完成了一个简单的C#调用动态链接库的案例 下面将通过一个具体实例,讲述为什么要这样做?(本实例通过从性能方面进行对比) 在DLL中的头文件中,加入如下代码: HEAD void CBubbleSort(int* array, int length); 在.c文件中加入如下代码: HEAD void CBubbleSort(int* array, int length) { int temp = 0; for (int i = 0; i < length; i++) { for (int j = i + 1; j < length; j++) { if (array[i] > array[j]) { temp = array[i]; array[i] = array[j]; array[j] = temp; } } } } C#中的代码修改: using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; class Program { const string NativeLib = "NativeDll.dll"; [DllImport(NativeLib)] public unsafe static extern void CBubbleSort(int* arr, int length); static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64", "Release", "NativeDll.dll"); //Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } public unsafe static void Main(string[] args) { int num = 10000; int[] arr = new int[num]; int[] cSharpResult = new int[num]; //随机生成num数量个(0-10000)的数字 Random random = new Random(); for (int i = 0; i < arr.Length; i++) { arr[i] = random.Next(10000); } //利用冒泡排序对其数组进行排序 Stopwatch sw = Stopwatch.StartNew(); Array.Copy(arr, cSharpResult, arr.Length); cSharpResult = BubbleSort(cSharpResult); Console.WriteLine($"\n C#实现排序所耗时:{sw.ElapsedMilliseconds}ms\n"); // 调用Dll中的冒泡排序算法 NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); fixed (int* ptr = &arr[0]) { sw.Restart(); CBubbleSort(ptr, arr.Length); } Console.WriteLine($"\n C实现排序所耗时:{sw.ElapsedMilliseconds}ms"); Console.ReadKey(); } //冒泡排序算法 public static int[] BubbleSort(int[] array) { int temp = 0; for (int i = 0; i < array.Length; i++) { for (int j = i + 1; j < array.Length; j++) { if (array[i] > array[j]) { temp = array[i]; array[i] = array[j]; array[j] = temp; } } } return array; } } 执行结果: C#实现排序所耗时: 130ms C实现排序所耗时:3ms 在实现本案例中,可能在编译后,大家所看到的结果不是很出乎意料,但这只是一种案例,希望通过此案例的分析,能给大家带来一些意想不到的收获叭。 最后 简单做一下总结叭,通过上述所描述的从第一步如何创建一个DLL到如何通过C#去调用的一个简单实例,也应该能给正在查阅相关资料的你有所收获,也希望能给在这方面有所研究的你有一些相关的启发,同时也希望能给目前对这方面毫无了解的你有一个更进一步的学习。 作者:百宝门-刘忠帅 原文地址:https://blog.baibaomen.com/p-invoke之c调用动态链接库dll/
2024-01-19
200
0
0
网络聚合
2024-01-19
重新定义性价比!人工智能AI聊天ChatGPT新接口模型gpt-3.5-turbo闪电更新,成本降90%,Python3.10接入
北国春迟,春寒料峭略带阴霾,但ChatGPT新接口模型gpt-3.5-turbo的更新为我们带来了一丝暖意,使用成本更加亲民,比高端产品ChatGPT Plus更实惠也更方便,毕竟ChatGPT Plus依然是通过网页端来输出,Api接口是以token的数量来计算价格的,0.002刀每1000个token,token可以理解为字数,说白了就是每1000个字合0.01381人民币,以ChatGPT无与伦比的产品力而言,如此低的使用成本让所有市面上其他所有类ChatGPT产品都黯然失光。 本次让我们使用Python3.10光速接入ChatGPT API的新模型gpt-3.5-turbo。 OpenAI库的SDK方式接入 OpenAI官方同步更新了接口Api的三方库openai,版本为0.27.0,如果要使用新的模型gpt-3.5-turbo,就必须同步安装最新版本: pip3 install openai==0.27.0 随后建立chat.py文件: import openai openai.api_key = "openai的接口apikey" completion = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": "北国风光,千里冰封,万里雪飘,请接着续写,使用沁园春的词牌"}] ) print(completion["choices"][0]["message"]["content"]) 程序返回: 瑶池冰缘,雪舞凄美, 隔窗寒意,似乎钻进衣袖。 寒塘渡鸭,雪中梅影, 孤独是一片银白的姿态。 冰雪如花,开放在草莓园里, 可爱的雪人,瑟瑟发抖着欢呼。 北风凛冽,寒暄难挡, 四季明媚,但冬日尤甜美。 千里冰封,万里雪飘, 窗外天下壮观,此时正是京城美。 闪电般秒回,让用惯了ChatGPT网页端的我们几乎不能适应。 gpt-3.5-turbo,对得起turbo的加成,带涡轮的ChatGPT就是不一样。 ChatGPT聊天上下文 我们知道ChatGPT的最大特色就是可以联系语境中的上下文,换句话说,ChatGPT可以根据之前的回答来优化之后的回答,形成上下文关系,让人机对话更加连贯和富有逻辑性。 这里取决于输入参数中的role参数,每一个role的取值,对应的场景不一样,其中system用于在对话开始时给ChatGPT一个指示或声明,有点像引导词,使得后续的回答更具有个性化和专业化。user是用于给用户提问的或者说是用来给用户输入引导词的。assistant顾名思义,是用于输入ChatGPT的回答内容: import openai openai.api_key = "apikey" class ChatGPT: def init(self,chat_list=[]) -> None: # 初始化对话列表 self.chat_list = [] # 显示接口返回 def show_conversation(self,msg_list): for msg in msg_list: if msg['role'] == 'user': print(f"Me: {msg['content']}\n") else: print(f"ChatGPT: {msg['content']}\n") # 提示chatgpt def ask(self,prompt): self.chat_list.append({"role":"user","content":prompt}) response = openai.ChatCompletion.create(model="gpt-3.5-turbo",messages=self.chat_list) answer = response.choices[0].message['content'] # 添加历史对话,形成上下文关系 self.chat_list.append({"role":"assistant","content":answer}) self.show_conversation(self.chat_list) 这里每一次会话都会加入chat_list的聊天列表,角色为assistant,为了让ChatGPT分析上下文,下面开始测试: if __name__ == '__main__': chat = ChatGPT() chat.ask("你是一位南宋词人,词风婉约,有点类似李清照女士,请使用蝶恋花词牌描写北国春光") 程序返回: Me: 你是一位南宋词人,词风婉约,有点类似李清照女士,请使用蝶恋花词牌描写北国春光 ChatGPT: 北国春光,清冽宜人。望眼欲穿的远山如翠起伏,遥远而缥缈。层层叠叠的林木,新绿徜徉,婆娑摇曳。风儿吹起,沁人心脾,点点梅花飘至,宛如仙境。 花间蝶恋,春色满园。莺莺燕燕,鸟鸣花落,时时惹人遐思。碧空万里,蓝天白云,彩云飘飘,缤纷夺目。柳絮飘飘,轻羽翩翩,小河潺潺,流水声声,婉转动人。 清风拂面,落英缤纷。听着草虫唱起,充满阳光的气息,轻轻飘荡,仿佛一条无形的小河,展开春天的美好,留下美好的记忆。人间万象,却只有这春色无边,似乎奔向远方的快乐。 此时再次发问: chat.ask("请使用另外一种粗狂阳刚的风格再写一遍上面的词") 程序返回: Me: 请使用另外一种粗狂阳刚的风格再写一遍上面的词 ChatGPT: 北国春光,不柔不媚,金色的阳光照在地上,充满了男子气概。 草原上风吹不断,那些疯狂的野花,在春风中舞蹈。 看!那些猛禽静静地盘旋在高空,监视着整片草原,威武雄壮。 花丛间,一只雄性蜂鹰跃跃欲飞,看上去仿佛要冲破天际。 这里的春天有时带着风沙,但这并不能阻止狂放豪迈的草原奔腾前行,而这样的北国春光,怎会轻易被遗忘! 虽然内容有些尬,但确实联系了上下文。 需要注意的是,token不仅计算ChatGPT的接口返回内容,也会计算用户的发送内容,token的计算方法不是简单的一词一个,例如中文输入,一个中文汉字占2个字节数,而对于一次中文测试中,50个汉字被算为100个tokens,差不多是英文的一倍,而token还计算api发送中的角色字段,如果像上文一样实现上下文操作,就必须发送ChatGPT接口返回的历史聊天列表,这意味着ChatGPT上下文聊天的成本并不是我们想象中的那么低,需要谨慎使用。 原生ChatGPT接口异步访问 除了官方的SDK,新接口模型也支持原生的Http请求方式,比如使用requests库: pip3 install requests 直接请求openai官方接口: import requests h = { 'Content-Type': 'application/json', 'Authorization': 'Bearer apikey' } d = { "model": "gpt-3.5-turbo", "messages":[{"role": "user", "content": "请解释同步请求和异步请求的区别"}], "max_tokens": 100, "temperature": 0 } u = 'https://api.openai.com/v1/chat/completions' r = requests.post(url=u, headers=h, json=d).json() print(r) 程序返回: {'id': 'chatcmpl-6qDNQ9O4hZPDT1Ju902coxypjO0mY', 'object': 'chat.completion', 'created': 1677902496, 'model': 'gpt-3.5-turbo-0301', 'usage': {'prompt_tokens': 20, 'completion_tokens': 100, 'total_tokens': 120}, 'choices': [{'message': {'role': 'assistant', 'content': '\n\n同步请求和异步请求是指在客户端向服务器发送请求时,客户端等待服务器响应的方式不同。\n\n同步请求是指客户端发送请求后,必须等待服务器响应后才能继续执行后续的代码。在等待服务器响应的过程中,客户端的界面会被阻塞,用户无法进行'}, 'finish_reason': 'length', 'index': 0}]} ChatGPT原生接口也支持异步方式请求,这里使用httpx: pip3 install httpx 编写异步请求: h = { 'Content-Type': 'application/json', 'Authorization': 'Bearer apikey' } d = { "model": "gpt-3.5-turbo", "messages":[{"role": "user", "content": "请解释同步请求和异步请求的区别"}], "max_tokens": 100, "temperature": 0 } u = 'https://api.openai.com/v1/chat/completions' import asyncio import httpx async def main(): async with httpx.AsyncClient() as client: resp = await client.post(url=u, headers=h, json=d) result = resp.json() print(result) asyncio.run(main()) 程序返回: {'id': 'chatcmpl-6qDNQ9O4hZPDT1Ju902coxypjO0mY', 'object': 'chat.completion', 'created': 1677902496, 'model': 'gpt-3.5-turbo-0301', 'usage': {'prompt_tokens': 20, 'completion_tokens': 100, 'total_tokens': 120}, 'choices': [{'message': {'role': 'assistant', 'content': '\n\n同步请求和异步请求是指在客户端向服务器发送请求时,客户端等待服务器响应的方式不同。\n\n同步请求是指客户端发送请求后,必须等待服务器响应后才能继续执行后续的代码。在等待服务器响应的过程中,客户端的界面会被阻塞,用户无法进行'}, 'finish_reason': 'length', 'index': 0}]} 我们也可以将异步请求方式封装到对话类中,完整代码: import openai import asyncio import httpx openai.api_key = "apikey" h = { 'Content-Type': 'application/json', 'Authorization': f'Bearer ' } d = { "model": "gpt-3.5-turbo", "messages":[{"role": "user", "content": "请解释同步请求和异步请求的区别"}], "max_tokens": 100, "temperature": 0 } u = 'https://api.openai.com/v1/chat/completions' class ChatGPT: def init(self,chat_list=[]) -> None: # 初始化对话列表 self.chat_list = [] # 异步访问 async def ask_async(self,prompt): d["messages"][0]["content"] = prompt async with httpx.AsyncClient() as client: resp = await client.post(url=u, headers=h, json=d) result = resp.json() print(result) # 显示接口返回 def show_conversation(self,msg_list): for msg in msg_list: if msg['role'] == 'user': print(f"Me: {msg['content']}\n") else: print(f"ChatGPT: {msg['content']}\n") # 提示chatgpt def ask(self,prompt): self.chat_list.append({"role":"user","content":prompt}) response = openai.ChatCompletion.create(model="gpt-3.5-turbo",messages=self.chat_list) answer = response.choices[0].message['content'] # 添加历史对话,形成上下文关系 self.chat_list.append({"role":"assistant","content":answer}) self.show_conversation(self.chat_list) if name == 'main': chat = ChatGPT() chat.ask("你是一位南宋词人,词风婉约,有点类似李清照女士,请使用蝶恋花词牌描写北国春光") chat.ask("请使用另外一种粗狂阳刚的风格再写一遍上面的词") asyncio.run(chat.ask_async("请解释同步请求接口和异步请求接口的区别")) 结语 低成本ChatGPT接口模型gpt-3.5-turbo更容易接入三方的客户端,比如微信、QQ、钉钉群之类,比起ChatGPT网页端,ChatGPT接口的响应速度更加迅速且稳定,ChatGPT,永远的神,没有之一,且不可替代,最后奉上异步上下文封装项目,与君共觞:github.com/zcxey2911/chatgpt_api_Contextual_async
2024-01-19
312
0
0
网络聚合
2024-01-19
如何在多个应用程序中共享日志配置
有的时候你有多个应用程序,它们需要使用相同的日志配置。在这种情况下,你可以将日志配置放在一个共享的位置,然后通过项目文件快速引用。方便快捷,不用重复配置。 Directory.Build.props 通过在项目文件夹中创建一个名为 Directory.Build.props 的文件,可以将配置应用于所有项目。这个文件的内容如下: <Project> <ItemGroup Condition="$(MyApplication) == 'true'"> <Content Include="..\Shared\appsettings.logging.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <Link>Shared\appsettings.logging.json</Link> </Content> </ItemGroup></Project> 我们可以将这个文件放在解决方案文件夹的根目录中,这样就可以将配置应用于所有项目。 由于我们定义了一个条件,所以我们可以通过设置 MyApplication 属性来控制是否应用这个配置。在这个例子中,我们将 MyApplication 属性设置为 true,所以我们只要在项目文件中设置这个属性,就可以应用这个配置。 项目文件 在项目文件中,我们需要设置 MyApplication 属性,然后引用 Directory.Build.props 文件。这样就可以应用 Directory.Build.props 文件中的配置了。 <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net7.0</TargetFramework> <MyApplication>true</MyApplication> </PropertyGroup></Project> appsettings.logging.json 在 Shared 文件夹中,我们需要创建一个名为 appsettings.logging.json 的文件,这个文件就是我们的日志配置文件。这个文件的内容如下: { "Serilog": { "MinimumLevel": { "Default": "Information", "Override": { "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "WriteTo": [ { "Name": "Console", "Args": { "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}" } } ] }} 使用 appsettings.logging.json 在 Program.cs 文件中,我们需要将日志配置文件的路径传递给 CreateHostBuilder 方法。这样就可以使用 appsettings.logging.json 文件中的配置了。 private void LoadSharedAppSettings(WebApplicationBuilder builder){ var appsettingsParts = new[] { "logging" }; var sharedBaseDir = Path.Combine(AppContext.BaseDirectory, "Shared"); foreach (var appsettingsPart in appsettingsParts) { builder.Configuration.AddJsonFile(Path.Combine(sharedBaseDir, $"appsettings.{appsettingsPart}.json"), true, true); builder.Configuration.AddJsonFile( Path.Combine(sharedBaseDir, $"appsettings.{appsettingsPart}.{builder.Environment.EnvironmentName}.json"), true, true); }} 文件夹结构 最后我们看一下文件夹的结构: ├───MyApplication1 │ ├───Properties │ └───wwwroot ├───MyApplication2 │ ├───Properties │ └───wwwroot ├───Shared │ └───appsettings.logging.json └───MyApplication.sln 总结 通过在项目文件夹中创建一个名为 Directory.Build.props 的文件,可以将配置应用于所有项目。在项目文件中,我们需要设置 MyApplication 属性,然后引用 Directory.Build.props 文件。在 Program.cs 文件中,我们需要将日志配置文件的路径传递给 CreateHostBuilder 方法。这样就可以使用 appsettings.logging.json 文件中的配置了。 参考资料 Directory.Build.props[1] appsettings.json[2] 本文作者: newbe36524 本文链接: https://www.newbe.pro/ChatAI/0x015-How-to-share-logging-configuration-in-multiple-applications/ 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处! 参考资料 [1] Directory.Build.props: https://learn.microsoft.com/visualstudio/msbuild/customize-your-build?view=vs-2022&WT.mc_id=DX-MVP-5003606#directorybuildprops-and-directorybuildtargets [2] appsettings.json: https://learn.microsoft.com/aspnet/core/fundamentals/configuration/?view=aspnetcore-7.0&WT.mc_id=DX-MVP-5003606#appsettingsjson
2024-01-19
237
0
0
网络聚合
2024-01-19
Docker 与 Linux Cgroups:资源隔离的魔法之旅
这篇文章主要介绍了 Docker 如何利用 Linux 的 Control Groups(cgroups)实现容器的资源隔离和管理。 最后通过简单 Demo 演示了如何使用 Go 和 cgroups 交互。 如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。 搜索公众号【探索云原生】即可订阅 1.Docker 是如何使用 Cgroups 的 我们知道 Docker 是通过 Cgroups 实现容器资源限制和监控的,那么具体是怎么用的呢? 演示 包含以下步骤: 1)创建容器,指定内存限制 2)查看 cgroup 情况 3)停止容器 4)再次查看 cgroup 情况 先启动一个容器: [root@iZ2zefmrr626i66omb40ryZ memory]# docker run -itd -m 128m nginx da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416 这里通过-m参数设置了内存限制为 128M。 该命令执行后 docker 会在 memory cgroup 上(也就是 /sys/fs/cgroup/memory 路径下)创建一个叫 docker 的子 cgroup,具体如下: $ ls -l /sys/fs/cgroup/memory/docker/ -rw-r--r-- 1 root root 0 Jan 6 19:53 cgroup.clone_children --w--w--w- 1 root root 0 Jan 6 19:53 cgroup.event_control -rw-r--r-- 1 root root 0 Jan 6 19:53 cgroup.procs # 可以发现这一长串ID和创建容器时打印的是一致的 drwxr-xr-x 2 root root 0 Jan 6 19:56 da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416 # 省略其他文件 内部除了 cgroup 相关的文件外,还有很多目录,使用容器 ID 作为目录名,其中每个目录即对应一个容器。 其中,da82f9e...这个目录名称和容器 ID 一致,说明 docker 是为每个容器创建了一个子 cgroup 来单独限制。 查看一下里面的具体配置: [root@iZ2zefmrr626i66omb40ryZ docker]# cd da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416/ [root@iZ2zefmrr626i66omb40ryZ da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416]# cat memory.limit_in_bytes 134217728 可以发现,memory.limit_in_bytes 中配置的值为 134217728,转换一下134217728/1024/1024=128M, 刚好就是我们指定的 128M。 然后我们停止该容器 docker stop da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416 再次查看 cgroup 情况 ls -l /sys/fs/cgroup/memory/docker/|grep da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416 发现目录已经被删除,说明容器对应的子 cgroup 也同步被回收。 把停止的容器 start 一下看看 docker start da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416 再次查看 cgroup 情况 [root@docker ~]# ls -l /sys/fs/cgroup/memory/docker/|grep da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416 drwxr-xr-x 2 root root 0 Jan 6 19:58 da82f9ebd384730dda7f831b4331c9e55893c100c83c0c9b0ce112436aa93416 可以看到,同名目录又被创建出来了。 至此,演示完成。 结论:Docker 容器启动时创建容器 ID 同名子 group 以实现资源控制,容器停止时删除该子 cgroup。 Demo 中只演示了内存限制,其他资源也是类似的 小结 所以 docker 使用 cgroup 其实很简单, 1)为每个容器创建一个子 cgroup 2)根据 docker run 时提供的参数调整 cgroup 中的配置 3)容器被删除时同步删除对应子 cgroup 2.Cgroups 相关操作命令 这里记录一下 cgroups 的一些常用操作命令。 hierarchy 创建 由于 Linux Cgroups 是基于内核中的 cgroup virtual filesystem 的,所以创建 hierarchy 其实就是将其挂载到指定目录。 语法为: mount -t cgroup -o subsystems name /cgroup/name 其中 subsystems 表示需要挂载的 cgroups 子系统 /cgroup/name 表示挂载点(一般为具体目录) 这条命令同在内核中创建了一个 hierarchy 以及一个默认的 root cgroup。 例如: $ mkdir cg1 $ mount -t cgroup -o cpuset cg1 ./cg1 比如以上命令就是挂载一个 cg1 的 hierarchy 到 ./cg1 目录,如果指定的 hierarchy 不存在则会新建。 hierarchy 创建的时候就会就会自动创建一个 cgroup 以作为 cgroup 树中的 root 节点。 删除 删除 hierarchy 则是卸载。 语法为:umount /cgroup/name /cgroup/name 表示挂载点(一般为具体目录) 例如: $ umount ./cg1 以上命令就是卸载 ./cg1 这个目录上挂载的 hierarchy,也就是前面挂载的 cg。 hierarchy 卸载后,相关的 cgroup 都会被删除。 不过 cg1 目录需要手动删除。 默认文件含义 hierarchy 挂载后会生成一些文件,具体如下: 为了避免干扰,未关联任何 subsystem $ mkdir cg1 $ mount -t cgroup -o none,name=cg1 cg1 ./cg1 $ tree cg1 cg1 ├── cgroup.clone_children ├── cgroup.procs ├── cgroup.sane_behavior ├── notify_on_release ├── release_agent └── tasks 具体含义如下: cgroup.clone_children:这个文件只对 cpuset subsystem 有影响,当该文件的内容为 1 时,新创建的 cgroup 将会继承父 cgroup 的配置,即从父 cgroup 里面拷贝配置文件来初始化新 cgroup,可以参考cgroup.clone_children cgroup.procs:当前 cgroup 中的所有进程ID,系统不保证 ID 是顺序排列的,且 ID 有可能重复 cgroup.sane_behavior:这个文件只会存在于 root cgroup 下面,用于控制某些特性的开启和关闭。 由于 cgroup 一直再发展,很多子系统有很多不同的特性,因此内核用CGRP_ROOT_SANE_BEHAVIOR来控制 notify_on_release:该文件的内容为 1 时,当 cgroup 退出时(不再包含任何进程和子 cgroup),将调用 release_agent 里面配置的命令。 新 cgroup 被创建时将默认继承父 cgroup 的这项配置。 release_agent:里面包含了 cgroup 退出时将会执行的命令,系统调用该命令时会将相应 cgroup 的相对路径当作参数传进去。 注意:这个文件只会存在于 root cgroup 下面,其他 cgroup 里面不会有这个文件。 相当于配置一个回调用于清理资源。 tasks:当前 cgroup 中的所有线程 ID,系统不保证 ID 是顺序排列的 cgroup.procs 和 tasks 的区别见 cgroup 操作章节。 release_agent 演示 当一个 cgroup 里没有进程也没有子 cgroup 时,release_agent 将被调用来执行 cgroup 的清理工作。 具体操作流程: 首先需要配置 notify_on_release 以开启该功能。 然后将脚本内容写入到 release_agent 中去。 最后 cgroup 退出时(不再包含任何进程和子 cgroup)就会执行 release_agent 中的命令。 #创建新的cgroup用于演示 dev@ubuntu:~/cgroup/demo$ sudo mkdir test #先enable release_agent dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 1 > ./test/notify_on_release' #然后创建一个脚本/home/dev/cgroup/release_demo.sh, #一般情况下都会利用这个脚本执行一些cgroup的清理工作,但我们这里为了演示简单,仅仅只写了一条日志到指定文件 dev@ubuntu:~/cgroup/demo$ cat > /home/dev/cgroup/release_demo.sh << EOF #!/bin/bash echo $0:$1 >> /home/dev/release_demo.log EOF #添加可执行权限 dev@ubuntu:~/cgroup/demo$ chmod +x ../release_demo.sh #将该脚本设置进文件release_agent dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo /home/dev/cgroup/release_demo.sh > ./release_agent' dev@ubuntu:~/cgroup/demo$ cat release_agent /home/dev/cgroup/release_demo.sh #往test里面添加一个进程,然后再移除,这样就会触发release_demo.sh dev@ubuntu:~/cgroup/demo$ echo $$ 27597 dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 27597 > ./test/cgroup.procs' dev@ubuntu:~/cgroup/demo$ sudo sh -c 'echo 27597 > ./cgroup.procs' #从日志可以看出,release_agent被触发了,/test是cgroup的相对路径 dev@ubuntu:~/cgroup/demo$ cat /home/dev/release_demo.log /home/dev/cgroup/release_demo.sh:/test cgroup 创建 创建 cgroup 很简单,在父 cgroup 或者 hierarchy 目录下新建一个目录就可以了。 具体层级关系就和目录层级关系一样。 # 创建子cgroup cgroup-cpu $ mkdir cgroup-cpu $ cd cgroup-cpu # 创建cgroup-cpu的子cgroup $ mkdir cgroup-cpu-1 删除 删除也很简单,删除对应目录即可。 注意:是删除目录 rmdir,而不是递归删除目录下的所有文件。 如果有多层 cgroup 则需要先删除子 cgroup,否则会报错: $ rmdir cgroup-cpu # 如果cgroup中有进程正在本限制,也会出现这个错误,需要先停掉对应进程,或者把进程移动到另外的 cgroup 中(比如父cgroup) rmdir: failed to remove 'cgroup-cpu': Device or resource busy 先删除子 cgroup 就可以了: $ rmdir cg1 $ cd ../ $ rmdir cgroup-cpu 也可以借助 libcgroup 工具来创建或删除。 使用 libcgroup 工具前,请先安装 libcgroup 和 libcgroup-tools 数据包 redhat 系统安装: $ yum install libcgroup $ yum install libcgroup-tools ubuntu 系统安装: $ apt-get install cgroup-bin # 如果提示cgroup-bin找不到,可以用 cgroup-tools 替换 $ apt-get install cgroup-tools 具体语法: # controllers就是subsystem # path可以用相对路径或者绝对路径 $ cgdelete controllers:path 例如: $ cgcreate cpu:./mycgroup $ cgdelete cpu:./mycgroup 添加进程 创建新的 cgroup 后,就可以往里面添加进程了。注意下面几点: 在一颗 cgroup 树里面,一个进程必须要属于一个 cgroup。 所以不能凭空从一个 cgroup 里面删除一个进程,只能将一个进程从一个 cgroup 移到另一个 cgroup 新创建的子进程将会自动加入父进程所在的 cgroup。 这也就是 tasks 和 cgroup.proc 的区别。 从一个 cgroup 移动一个进程到另一个 cgroup 时,只要有目的 cgroup 的写入权限就可以了,系统不会检查源 cgroup 里的权限。 用户只能操作属于自己的进程,不能操作其他用户的进程,root 账号除外。 #--------------------------第一个shell窗口---------------------- #创建一个新的cgroup dev@ubuntu:~/cgroup/demo$ sudo mkdir test dev@ubuntu:~/cgroup/demo$ cd test #将当前bash加入到上面新创建的cgroup中 dev@ubuntu:~/cgroup/demo/test$ echo $$ 1421 dev@ubuntu:~/cgroup/demo/test$ sudo sh -c 'echo 1421 > cgroup.procs' #注意:一次只能往这个文件中写一个进程ID,如果需要写多个的话,需要多次调用这个命令 #--------------------------第二个shell窗口---------------------- #重新打开一个shell窗口,避免第一个shell里面运行的命令影响输出结果 #这时可以看到cgroup.procs里面包含了上面的第一个shell进程 dev@ubuntu:~/cgroup/demo/test$ cat cgroup.procs 1421 #--------------------------第一个shell窗口---------------------- #回到第一个窗口,随便运行一个命令,比如 top dev@ubuntu:~/cgroup/demo/test$ top #这里省略输出内容 #--------------------------第二个shell窗口---------------------- #这时再在第二个窗口查看,发现top进程自动加入了它的父进程(1421)所在的cgroup dev@ubuntu:~/cgroup/demo/test$ cat cgroup.procs 1421 16515 dev@ubuntu:~/cgroup/demo/test$ ps -ef|grep top dev 16515 1421 0 04:02 pts/0 00:00:00 top dev@ubuntu:~/cgroup/demo/test$ #在一颗cgroup树里面,一个进程必须要属于一个cgroup, #所以我们不能凭空从一个cgroup里面删除一个进程,只能将一个进程从一个cgroup移到另一个cgroup, #这里我们将1421移动到root cgroup dev@ubuntu:~/cgroup/demo/test$ sudo sh -c 'echo 1421 > ../cgroup.procs' dev@ubuntu:~/cgroup/demo/test$ cat cgroup.procs 16515 #移动1421到另一个cgroup之后,它的子进程不会随着移动 #--------------------------第一个shell窗口---------------------- ##回到第一个shell窗口,进行清理工作 #先用ctrl+c退出top命令 dev@ubuntu:~/cgroup/demo/test$ cd .. #然后删除创建的cgroup dev@ubuntu:~/cgroup/demo$ sudo rmdir test cgroup.procs vs tasks #创建两个新的cgroup用于演示 dev@ubuntu:~/cgroup/demo$ sudo mkdir c1 c2 #为了便于操作,先给root账号设置一个密码,然后切换到root账号 dev@ubuntu:~/cgroup/demo$ sudo passwd root dev@ubuntu:~/cgroup/demo$ su root root@ubuntu:/home/dev/cgroup/demo# #系统中找一个有多个线程的进程 root@ubuntu:/home/dev/cgroup/demo# ps -efL|grep /lib/systemd/systemd-timesyncd systemd+ 610 1 610 0 2 01:52 ? 00:00:00 /lib/systemd/systemd-timesyncd systemd+ 610 1 616 0 2 01:52 ? 00:00:00 /lib/systemd/systemd-timesyncd #进程610有两个线程,分别是610和616 #将616加入c1/cgroup.procs root@ubuntu:/home/dev/cgroup/demo# echo 616 > c1/cgroup.procs #由于cgroup.procs存放的是进程ID,所以这里看到的是616所属的进程ID(610) root@ubuntu:/home/dev/cgroup/demo# cat c1/cgroup.procs 610 #从tasks中的内容可以看出,虽然只往cgroup.procs中加了线程616, #但系统已经将这个线程所属的进程的所有线程都加入到了tasks中, #说明现在整个进程的所有线程已经处于c1中了 root@ubuntu:/home/dev/cgroup/demo# cat c1/tasks 610 616 #将616加入c2/tasks中 root@ubuntu:/home/dev/cgroup/demo# echo 616 > c2/tasks #这时我们看到虽然在c1/cgroup.procs和c2/cgroup.procs里面都有610, #但c1/tasks和c2/tasks中包含了不同的线程,说明这个进程的两个线程分别属于不同的cgroup root@ubuntu:/home/dev/cgroup/demo# cat c1/cgroup.procs 610 root@ubuntu:/home/dev/cgroup/demo# cat c1/tasks 610 root@ubuntu:/home/dev/cgroup/demo# cat c2/cgroup.procs 610 root@ubuntu:/home/dev/cgroup/demo# cat c2/tasks 616 #通过tasks,我们可以实现线程级别的管理,但通常情况下不会这么用, #并且在cgroup V2以后,将不再支持该功能,只能以进程为单位来配置cgroup #清理 root@ubuntu:/home/dev/cgroup/demo# echo 610 > ./cgroup.procs root@ubuntu:/home/dev/cgroup/demo# rmdir c1 root@ubuntu:/home/dev/cgroup/demo# rmdir c2 root@ubuntu:/home/dev/cgroup/demo# exit exit 结论:将线程 ID 加到 cgroup1 的 cgroup.procs 时,会把线程对应进程 ID 加入 cgroup.procs 且还会把当前进程下的全部线程 ID 加入到 tasks 中。 这里看起来,进程和线程好像效果是一样的。 区别来了,如果此时把某个线程 ID 移动到另外的 cgroup2 的 tasks 中,会自动把 线程 ID 对应的进程 ID 加入到 cgroup2 的 cgroup.procs 中,且只把对应线程加入 tasks 中。 此时 cgroup1 和 cgroup2 的 cgroup.procs 都包含了同一个进程 ID,但是二者的 tasks 中却包含了不同的线程 ID。 这样就实现了线程粒度的控制。但通常情况下不会这么用,并且在 cgroup V2 以后,将不再支持该功能,只能以进程为单位来配置 cgroup。 3.如何使用 Go 和 Cgroups 交互 其实挺简单的,就是用 Go 翻译了一遍上面的命令。 后续则是按照这个流程实现自己的 docker。 具体代码如下: // cGroups cGroups初体验 func cGroups() { // /proc/self/exe是一个符号链接,代表当前程序的绝对路径 if os.Args[0] == "/proc/self/exe" { // 第一个参数就是当前执行的文件名,所以只有fork出的容器进程才会进入该分支 fmt.Printf("容器进程内部 PID %d\n", syscall.Getpid()) // 需要先在宿主机上安装 stress 比如 apt-get install stress cmd := exec.Command("sh", "-c", `stress --vm-bytes 200m --vm-keep -m 1`) cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Println(err) os.Exit(1) } } else { // 主进程会走这个分支 cmd := exec.Command("/proc/self/exe") cmd.SysProcAttr = &syscall.SysProcAttr{Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWNS | syscall.CLONE_NEWPID} cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { fmt.Println(err) os.Exit(1) } // 得到 fork 出来的进程在外部namespace 的 pid fmt.Println("fork 进程 PID:", cmd.Process.Pid) // 在默认的 memory cgroup 下创建子目录,即创建一个子 cgroup err := os.Mkdir(filepath.Join(cgroupMemoryHierarchyMount, "testmemorylimit"), 0755) if err != nil { fmt.Println(err) } // 将容器加入到这个 cgroup 中,即将进程PID加入到cgroup下的 cgroup.procs 文件中 err = ioutil.WriteFile(filepath.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "cgroup.procs"), []byte(strconv.Itoa(cmd.Process.Pid)), 0644) if err != nil { fmt.Println(err) os.Exit(1) } // 限制进程的内存使用,往 memory.limit_in_bytes 文件中写入数据 err = ioutil.WriteFile(filepath.Join(cgroupMemoryHierarchyMount, "testmemorylimit", "memory.limit_in_bytes"), []byte("100m"), 0644) if err != nil { fmt.Println(err) os.Exit(1) } cmd.Process.Wait() } } 首先是一个 if 判断,区分主进程和子进程,分别执行不同逻辑。 主进程:fork 出子进程,并创建 cgroup,然后将子进程加入该 cgrouop 子进程:执行 stress 命令,以消耗内存,便于查看 memory cgroup 的效果 运行并测试: lixd ~/projects/docker/mydocker main $ go build main.go lixd ~/projects/docker/mydocker main $ sudo ./main fork 进程 PID: 21827 当前进程 pid 1 stress: info: [7] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd 根据输出可以知道,我们 fork 出的进程,pid 为 21827。 通过pstree -pl查看进程关系: $pstree -pl init(1)─┬─init(8)───init(9)───fsnotifier-wsl(10) ├─init(12)───init(13)─┬─exe(20618)─┬─sh(20623)───stress(20624)───stress(20625) │ │ ├─{exe}(20619) │ │ ├─{exe}(20620) │ │ ├─{exe}(20621) │ │ └─{exe}(20622) └─zsh(14)───sudo(21821)───main(21822)─┬─exe(21827)─┬─sh(21832)───stress(21833)───stress(21834) 可以看到 21827 进程 最终启动了一个 21834 的 stress 进程。 top查看以下内存占用: PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 21834 root 20 0 208664 101564 272 D 35.2 1.3 0:14.38 stress 可以看到 RES 101564,也就是刚好 100M,说明我们的 cgroup 是有效果的。 4. 小结 本文主要介绍了 1)Docker 是如何使用 cgroups 的; 2) hierarchy 和 cgroup 相关的操作,如创建删除等; 3)最后则是简单演示了如何使用 Go 和 cgroups 进行交互。 至此,cgroups 的相关内容就告一段落了,加上本文一共包括 3 篇文章: 初探 Linux Cgroups:资源控制的奇妙世界 深入剖析 Linux Cgroups 子系统:资源精细管理 包括以下内容: 1)cgroups 怎么实现资源控制的 2)相关 subsystem 演示 3)docker 怎么使用 cgroups 的 4)go 怎么操作 cgroups 后续可以使用 go 实现 docker 的时候,资源控制就会使用 go 和 cgroups 交互来实现。 如果你对云原生技术充满好奇,想要深入了解更多相关的文章和资讯,欢迎关注微信公众号。 搜索公众号【探索云原生】即可订阅 5.参考 cgroups(7) — Linux manual page Linux Cgroup 系列(02):创建并管理 cgroup cgroup 源码分析 6——cgroup 中默认控制文件的内核实现分析
2024-01-19
462
0
0
网络聚合
2024-01-19
MySQL驱动扯后腿?Spring Boot用虚拟线程可能比用物理线程还差
之前已经分享过多篇关于Spring Boot中使用Java 21新特性虚拟线程的性能测试案例: Spring Boot 3.2虚拟线程搭建静态文件服务器有多快? Spring Boot 虚拟线程与Webflux在JWT验证和MySQL查询上的性能比较 早上看到群友问到一个关于虚拟线程遇到MySQL连接不兼容导致的性能问题: 这个问题确实之前就有看到过相关的评测,顺着个这个问题,重新把相关评测找出来,给大家分享一下。 以下内容主要参考文章:https://medium.com/deno-the-complete-reference/springboot-physical-vs-virtual-threads-vs-webflux-performance-comparison-for-jwt-verify-and-mysql-23d773b41ffd 评测案例 评测采用现实场景中的处理流程,具体如下: 从HTTP授权标头(authorization header)中提取 JWT 验证 JWT 并从中提取用户的电子邮件 使用提取到的电子邮件执行 MySQL 查询用户 返回用户记录 这个场景其实是Spring Boot 虚拟线程与Webflux在JWT验证和MySQL查询上的性能比较测试的后续。前文主要对比了虚拟线程和WebFlux的,但没有对比虚拟线程与物理线程的区别。所以,接下来的内容就是本文关心的重点:在物理线程和虚拟线程下,MySQL驱动是否有性能优化。 测试环境 Java 20(使用预览模式,开启虚拟线程) Spring Boot 3.1.3 依赖的第三方库:jjwt、mysql-connector-java 测试工具:Bombardier 采用了开源负载测试工具:Bombardier。在测试场景中预先创建 100,000 个 JWT 列表。 在测试期间,Bombardier 从该池中随机选择了JWT,并将它们包含在HTTP请求的Authorization标头中。 MySQL表结构与数据准备 User表结构如下: mysql> desc users; +--------+--------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +--------+--------------+------+-----+---------+-------+ | email | varchar(255) | NO | PRI | NULL | | | first | varchar(255) | YES | | NULL | | | last | varchar(255) | YES | | NULL | | | city | varchar(255) | YES | | NULL | | | county | varchar(255) | YES | | NULL | | | age | int | YES | | NULL | | +--------+--------------+------+-----+---------+-------+ 6 rows in set (0.00 sec) 准备大约10w条数据: mysql> select count(*) from users; +----------+ | count(*) | +----------+ | 99999 | +----------+ 1 row in set (0.01 sec) 测试代码:使用物理线程 配置文件: server.port=3000 spring.datasource.url= jdbc:mysql://localhost:3306/testdb?useSSL=false&allowPublicKeyRetrieval=true spring.datasource.username= dbuser spring.datasource.password= dbpwd spring.jpa.hibernate.ddl-auto= update spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect User实体定义: @Entity @Table(name = "users") public class User { @Id private String email; private String first; private String last; private String city; private String county; private int age; // 省略了getter和setter } 数据访问实现: public interface UserRepository extends CrudRepository<User, String> { } API实现: @RestController public class UserController { @Autowired UserRepository userRepository; private SignatureAlgorithm sa = SignatureAlgorithm.HS256; private String jwtSecret = System.getenv("JWT_SECRET"); @GetMapping("/") public User handleRequest(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHdr) { String jwtString = authHdr.replace("Bearer",""); Claims claims = Jwts.parser() .setSigningKey(jwtSecret.getBytes()) .parseClaimsJws(jwtString).getBody(); Optional<User> user = userRepository.findById((String)claims.get("email")); return user.get(); } } 应用主类: @SpringBootApplication public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } } 测试代码:使用虚拟线程 主要调整应用主类,其他一样,具体修改如下: @SpringBootApplication public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } @Bean public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() { return protocolHandler -> { protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); }; } } 测试代码:使用WebFlux server.port=3000 spring.r2dbc.url=r2dbc:mysql://localhost:3306/testdb?allowPublicKeyRetrieval=true&ssl=false spring.r2dbc.username=dbuser spring.r2dbc.password=dbpwd spring.r2dbc.pool.initial-size=10 spring.r2dbc.pool.max-size=10 @Table(name = "users") public class User { @Id private String email; private String first; private String last; private String city; private String county; private int age; // 省略getter、setter和构造函数 } 数据访问实现: public interface UserRepository extends R2dbcRepository<User, String> { } 业务逻辑实现: @Service public class UserService { @Autowired UserRepository userRepository; public Mono<User> findById(String id) { return userRepository.findById(id); } } API实现: @RestController @RequestMapping("/") public class UserController { @Autowired UserService userService; private SignatureAlgorithm sa = SignatureAlgorithm.HS256; private String jwtSecret = System.getenv("JWT_SECRET"); @GetMapping("/") @ResponseStatus(HttpStatus.OK) public Mono<User> getUserById(@RequestHeader(HttpHeaders.AUTHORIZATION) String authHdr) { String jwtString = authHdr.replace("Bearer",""); Claims claims = Jwts.parser() .setSigningKey(jwtSecret.getBytes()) .parseClaimsJws(jwtString).getBody(); return userService.findById((String)claims.get("email")); } } 应用主类: @EnableWebFlux @SpringBootApplication public class UserApplication { public static void main(String[] args) { SpringApplication.run(UserApplication.class, args); } } 测试结果 每次测试都包含 100 万个请求,分别评估了它们在不同并发(50、100、300)水平下的性能。下面是结果展示: 分析总结 在这个测试案例中使用了MySQL驱动,虚拟线程的实现方式性能最差,WebFlux依然保持领先。所以,主要原因在于这个MySQL的驱动对虚拟线程不友好。如果涉及到数据库访问的情况下,需要寻找对虚拟线程支持最佳的驱动程序。另外,该测试使用的是Java 20和Spring Boot 3.1。对于Java 21和Spring Boot 3.2建议读者在使用的时候自行评估。 最后,对于MySQL驱动对虚拟线程支持好的,欢迎留言区推荐一下。如果您学习过程中如遇困难?可以加入我们超高质量的Spring技术交流群,参与交流与讨论,更好的学习与进步!更多Spring Boot教程可以点击直达!,欢迎收藏与转发支持! 欢迎关注我的公众号:程序猿DD。第一时间了解前沿行业消息、分享深度技术干货、获取优质学习资源
2024-01-19
342
0
0
网络聚合
2024-01-19
Hadoop节点的分类与作用
文件的数据类型 文件有一个stat命令 元数据信息-->描述文件的属性 文件有一个vim命令 查看文件的数据信息 分类 元数据 File 文件名 Size 文件大小(字节) Blocks 文件使用的数据块总数 IO Block 数据块的大小 regular file:文件类型(常规文件) Device 设备编号 Inode 文件所在的Inode Links 硬链接次数 Access 权限 Uid 属主id/用户 Gid 属组id/组名 Access Time:简写为atime,表示文件的访问时间。当文件内容被访问时,更新这个时间 Modify Time:简写为mtime,表示文件内容的修改时间,当文件的数据内容被修改时,更新这个时间。 Change Time:简写为ctime,表示文件的状态时间,当文件的状态被修改时,更新这个时间,例如文件的链接数,大小,权限,Blocks数。 文件数据 真实存在于文件中的数据 NameNode(NN) 功能 接受客户端的读写服务 NameNode存放文件与Block的映射关系 DataNode存放Block与DataNode的映射关系 保存文件的元数据信息 文件的归属 文件的权限 文件的大小时间 lock信息,但是block的位置信息不会持久化,需要每次开启集群的时候DN上报 收集Block的信息 系统启动时 NN关机的时候是不会存储任意的Block与DN的映射信息 DN启动的时候,会将自己节点上存储的Block信息汇报给NN NN接受请求之后重新生成映射关系 Block--DN3 如果某个数据块的副本数小于设置数,那么NN会将这个副本拷贝到其他节点 集群运行中 NN与DN保持心跳机制,三秒钟发送一次 <property> <description>Determines datanode heartbeat interval in seconds.</description> <name>dfs.heartbeat.interval</name> <value>3</value> </property> <property> <name>heartbeat.recheck.interval</name> <value>300000</value> </property> 如果客户端需要读取或者上传数据的时候,NN可以知道DN的健康情况 可以让客户端读取存活的DN节点 如果DN超过三秒没有心跳,就认为DN出现异常 - 不会让新的数据读写到DataNode - 客户访问的时候不提供异常结点的地址 如果DN超过10分钟+30秒没有心跳,那么NN会将当前DN存储的数据转存到其他节点 超时时长的计算公式为: timeout = 2 * heartbeat.recheck.interval + 10 * dfs.heartbeat.interval。 而默认的heartbeat.recheck.interval 大小为5分钟,dfs.heartbeat.interval默认为3秒。 性能 NameNode为了效率,将所有的操作都在内存中完成 NameNode不会和磁盘进行任何的数据交换 问题: 数据的持久化 数据保存在内存中,掉电易失 DataNode(DN) 功能 存放的是文件的数据信息和验证文件完整性的校验信息 数据会存放在硬盘上 1m=1条元数据 1G=1条元数据 NameNode非常排斥存储小文件,一般小文件在存储之前需要进行压缩 汇报 启动时 汇报之前先验证Block文件是否被损坏 向NN汇报当前DN上block的信息 运行中 向NN保持心跳机制 客户可以向DN读写数据 当客户端读写数据的时候,首先去NN查询file与block与dn的映射 然后客户端直接与dn建立连接,然后读写数据 SecondaryNameNode 传统解决方案 日志机制 做任何操作之前先记录日志 当NN下次启动的时候,只需要重新按照以前的日志“重做”一遍即可缺点 缺点 edits文件大小不可控,随着时间的发展,集群启动的时间会越来越长 有可能日志中存在大量的无效日志 优点 绝对不会丢失数据 拍摄快照 我们可以将内存中的数据写出到硬盘上 序列化 启动时还可以将硬盘上的数据写回到内存中 反序列化 缺点 关机时间过长 如果是异常关机,数据还在内存中,没法写入到硬盘 如果写出频率过高,导致内存使用效率低(stop the world) JVM 优点 启动时间较短 SNN解决方案 解决思路(日志edits+快照fsimage) 让日志大小可控 定时快照保存 NameNode文件目录 查看目录 解决方案 当我们启动一个集群的时候,会产生四个文件 edits_0000000000000000001 fsimage_00000000000000000 seen_txid VERSION 我们每次操作都会记录日志 -->edits_inprogress-000000001 随和时间的推移,日志文件会越来越大,当达到阈值的时候(64M 或 3600秒) dfs.namenode.checkpoint.period 每隔多久做一次checkpoint ,默认3600s dfs.namenode.checkpoint.txns 每隔多少操作次数做一次checkpoint,默认1000000次 fs.namenode.checkpoint.check.period 每个多久检查一次操作次数,默认60s 会生成新的日志文件 edits_inprogress-000000001 -->edits_0000001 创建新的日志文件edits_inprogress-0000000016 节点的分类与作用汇总图
2024-01-19
206
0
0
网络聚合
2024-01-19
vue3实现一个抽奖小项目
前言 在公司年会期间我做了个抽奖小项目,我把它分享出来,有用得着的可以看下。 浏览链接:http://xisite.top/original/luck-draw/index.html 项目链接:https://gitee.com/xi1213/luck-draw (欢迎star!) 项目截图: 实现目标 数据保存:无后端,纯前端实现,浏览器刷新或者关闭数据不能丢失。 姓名切换:点击中部开始按钮姓名快速切换。 奖项切换:奖项为操作人员手动切换设置。 历史记录:抽奖完成后需要有历史记录。 数据导入:允许参与人员的表格导入。 数据保存 无后台,纯前端实现而且需要刷新关闭浏览器数据不丢失,很容易便会想到使用localStorage,localStorage存入的数据具有持久性,不会因为刷新或关闭浏览器而变化(除非手动刻意的清除),有别于sessionstorage,localStorage的生命周期是永久,sessionstorage是浏览器或者标签页关闭。 因为存入的数据不是单纯的字符串,而是具有结构性的对象数组,所以需要配合JSON.stringify与JSON.parse来使用。这是存入数据的方法: localStorage.setItem("luckDrawHis", JSON.stringify(luckDrawHis));//JSON.stringify将json转换为字符串 这是读取数据的方法: JSON.parse(localStorage.getItem("luckDrawHis"))//JSON.parse将字符串转换为json 姓名切换 抽奖的方式是数据导入后,点击中间的圆形开始按钮,姓名便开始快速切换,再次点击按钮便停止姓名切换,弹出对话框显示当前姓名以及设置的奖项。 切换姓名利用了vue的数据响应式原理。先获取到所有的参与人员数据,然后乱序处理,最后循环展示,我这里每个姓名展示的时间为50毫秒,你也可以自己设置。这里的数组乱序我使用了洗牌算法,其实就是利用Math.random获取数组的随机下标,然后与最后一个元素进行位置交换。 //洗牌算法(乱序数组) function shuffle(arr) { let l = arr.length let index, temp while (l > 0) { index = Math.floor(Math.random() * l) temp = arr[l - 1] arr[l - 1] = arr[index] arr[index] = temp l-- } return arr; } //循环列表 function forNameList(list) { list = shuffle(list); for (let i = 0; i < list.length; i++) { setTimeout(() => { if (!isStop.value) { curName.value = list[i].name; (i == list.length - 1) && (forNameList(nameList.value));//数组耗尽循环 } }, 50 * i); } } 奖项切换 奖项切换直接使用elementPlus的单选框即可。 历史记录 每次点击抽奖出现结果时,将之前的抽奖结果取出来,然后把当前的结果添加到末尾。 点击抽奖历史按钮时再将所有历史数据取出来。 数据导入 由于需要导入人员表格数据,这里我使用了xlsx插件与file-saver插件来实现。 首先是下载模板。 将事先准备好的表格模板放在项目的public目录下。 点击下载模板按钮时直接调用以下方法即可,其中的saveAs是file-saver插件中的方法,传入路径与文件名即可。 import { saveAs } from 'file-saver'; //下载模板 function downTemp() { let fileName = "人员模板.xlsx";//文件名 let fileUrl = "./template/";//文件路径(路径相对index.html) saveAs(fileUrl + fileName, fileName); } 表格处理好, 点击导入按钮读取表格数据时使用的是xlsx插件,下面是读取数据的方法。 import * as XLSX from "xlsx"; //导入数据 function importData(e) { isLoading.value = true; let file = e.target.files[0]; //获取事件中的file对象 let fileReader = new FileReader(); //创建文件读取器 fileReader.onload = (event) => { let result = event.target.result; //获取读取的结果 let workBook = XLSX.read(result, { type: "binary" }); //XLSX读取返回的结果 let jsonData = XLSX.utils.sheet_to_json( workBook.Sheets[workBook.SheetNames[0]] ); //将读取结果转换为json tabData.value = []; jsonData.forEach((j) => { tabData.value.push({ name: j.姓名, age: j.性别, department: j.部门, }); }); //处理成需要的数据格式 localStorage.setItem("tabData", JSON.stringify(tabData.value));//数据存入本地 tabDataS.value = JSON.parse(localStorage.getItem("tabData"));//取出数据 emits("getNameList", tabData); isLoading.value = false; }; fileReader.readAsBinaryString(file); //开始读取文件 ((document.getElementsByClassName("inp-xlsx")[0]).value = ""); //置空选中的文件 }; 结语 项目很简单,但给我的时间很少,很多优化的地方都没做好,后面有时间了再优化下,顺便适配下移动端。 原文地址:https://xiblogs.top/?id=53
2024-01-19
211
0
0
网络聚合
2024-01-18
2023牛客寒假算法基础集训营3 A-I+K
A 题解 知识点:贪心。 把所有正偶数除成奇数,即可。 (人傻了没加 \(x>0\) WA2 时间复杂度 \(O(n)\) 空间复杂度 \(O(1)\) 代码 #include <bits/stdc++.h> using ll = long long; using namespace std; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n; cin >> n; ll ans = 0; for (int i = 1;i <= n;i++) { int x; cin >> x; while (x > 0 && x % 2 == 0) x /= 2; ans += x; } cout << ans << '\n'; return 0; } B 题解 知识点:数学,构造。 特判 \(n=2\) 无解。 可以先放边长为 \(\left\lceil \dfrac{n}{2} \right\rceil\) 正方形,随后边长每增加 \(1\) 需要最少 \(3\) 块,直到边长为 \(2 \cdot \left\lceil \dfrac{n}{2} \right\rceil\) 后,边长每增加 \(1\) 需要最少 \(5\) 块。以此类推,当边长为 \(\left[(k-1)\cdot\left\lceil \dfrac{n}{2} \right\rceil,k\cdot\left\lceil \dfrac{n}{2} \right\rceil \right),k \in \N^+\) 时,边长每增加 \(1\) 需要 \(2k-1\) 块积木。 显然,摆完第一轮边长为 \(\left\lceil \dfrac{n}{2} \right\rceil\) 后,剩下的 \(n - \left\lceil \dfrac{n}{2} \right\rceil\) 个积木,而 \(\left\lfloor \dfrac{n - \left\lceil \dfrac{n}{2} \right\rceil}{3} \right\rfloor \leq \left\lceil \dfrac{n}{2} \right\rceil\) ,因此不可能摆到需要 \(5\) 个积木的情况。 综上,边长最大值为 \(\left\lceil \dfrac{n}{2} \right\rceil + \left\lfloor \dfrac{n - \left\lceil \dfrac{n}{2} \right\rceil}{3} \right\rfloor\) 。 本题也可以用二分边长做。 (没考虑 \(n - \left\lceil \dfrac{n}{2} \right\rceil\) 大小,傻了吧唧的算了通式,不过可以出题了qwq 考虑 \(n\) 块积木,给定 \(m\) ,每块积木大小为 \(1 \times k,k \in \left[ 1,\left\lceil \dfrac{m}{2} \right\rceil \right]\) ,求能摆成正方形的边长最大值。 时间复杂度 \(O(1)\) 空间复杂度 \(O(1)\) 代码 #include <bits/stdc++.h> using ll = long long; using namespace std; bool solve() { ll n; cin >> n; if (n == 2) return false; ll a = (n + 1) / 2; ll ans = a + (n - a) / 3; cout << ans << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; } C 题解 知识点:构造。 \(n \leq 3\) 或 \(n = 7\) 时无解。 考虑 \(n\bmod 4 = 0,1,2,3\) 的情况。 \(n \bmod 4 = 0\) 时显然形如构造 \(3,4,1,2\) 的循环即可。 \(n \bmod 4 = 1\) 时,前 \(5\) 项构造成 \(4,5,1,2,3\) ,其余仿照 \(n \bmod 4 = 0\) 情况。 \(n \bmod 4 = 2\) 时,前 \(6\) 项构造成 \(4,5,6,1,2,3\) ,其余仿照 \(n \bmod 4 = 0\) 情况。 \(n \bmod 4 = 3\) 时,前 \(11\) 项构造分为 \(5\) 项和 \(6\) 项两组仿照 \(n \bmod 4 = 1,2\) 情况,其余仿照 \(n \bmod 4 = 0\) 情况。 时间复杂度 \(O(n)\) 空间复杂度 \(O(1)\) 代码 #include <bits/stdc++.h> using ll = long long; using namespace std; bool solve() { int n; cin >> n; if (n <= 3 || n == 7) return false; int m = 1; if (n % 4 == 1) { cout << "4 5 1 2 3" << ' '; m = 6; } else if (n % 4 == 2) { cout << "4 5 6 1 2 3" << ' '; m = 7; } else if (n % 4 == 3) { cout << "4 5 1 2 3 9 10 11 6 7 8" << ' '; m = 12; } for (int i = m;i <= n;i += 4) { cout << i + 2 << ' ' << i + 3 << ' ' << i << ' ' << i + 1 << ' '; } cout << '\n'; return true; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; //cin >> t; while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; } D 题解 知识点:博弈论。 这类题需要先对局面分类,每种局面考虑找到一组平衡的操作,即对于其中一人,无论另一人如何操作,他都可以在下一次操作后回到原来的局面。 考虑将 \(n\) 分奇偶情况: \(n\) 为偶数,小红每次可以选 \(1\) ,随后数变为奇数局面,小紫只有奇数因子能选,数又变为偶数局面。到最后,必然是小紫让数变为 \(0\) ,因为只有小紫能让数变为偶数。因此,偶数局面小红必胜。 \(n\) 为奇数,根据 \(n\) 为偶数的推理,发现奇数局面小红必败。 时间复杂度 \(O(1)\) 空间复杂度 \(O(1)\) 代码 #include <bits/stdc++.h> using ll = long long; using namespace std; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); ll n; cin >> n; if (n & 1) cout << "yukari" << '\n'; else cout << "kou" << '\n'; return 0; } E 题解 知识点:计算几何。 设 \(A(x_A,y_A),B(x_B,y_B),C(x_C,y_C)\) 构成等腰直角三角形,其中 \(C\) 为顶点且在 \(AB\) 右侧,满足方程: \[\left\{ \begin{aligned} x_C+y_C = x_A + y_B\\ x_C-y_C = x_B - y_A \end{aligned} \right. \] 方程可以通过全等三角形证明。 显然 \(C\) 和 \(C\) 关于 \(AB\) 的对称点同时是或不是整数点,解出 \(C(x_C,y_C)\) 后判断是否为整数即可。 (平面几何永远的痛,并且以为无解输出-1收获WA 时间复杂度 \(O(1)\) 空间复杂度 \(O(1)\) 代码 #include <bits/stdc++.h> using ll = long long; using namespace std; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); ll a, b, c, d; cin >> a >> b >> c >> d; ll x = a + d + c - b; ll y = a + d + b - c; if (x & 1 || y & 1)cout << "No Answer!" << '\n'; else cout << x / 2 << ' ' << y / 2 << '\n'; return 0; } F 题解 知识点:宇宙的终极答案。 通过你高超的中文流读取技术,发现这是营销号特有的文案。 本打算对此嗤之以鼻的你,阅读完样例后逐渐理解了一切,确信 \(42\) 就是宇宙的终极答案。 时间复杂度 \(O(\infin)\) 空间复杂度 \(O(\infin)\) 代码 #include <bits/stdc++.h> using ll = long long; using namespace std; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cout << 42 << '\n'; return 0; } G 题解 知识点:模拟,枚举,dfs。 很简单(痛苦)的模拟。 dfs枚举每个 ? 的三种可能即可,注意快速幂前把底数模一下,因为可能炸 long long 。 可以选择预处理数字后边枚举边求值,也可以考虑枚举完再求值。注意,边枚举边求值不太适用于有优先级表达式。 (被表达式求值整了一顿,码力太差了QAQ 时间复杂度 \(O(3^{12} \cdot n)\) 空间复杂度 \(O(n)\) 代码 边枚举边求值 #include <bits/stdc++.h> using namespace std; using ll = long long; ll qpow(ll a, ll k, ll P) { ll ans = 1; while (k) { if (k & 1) ans = ans * a % P; k >>= 1; a = a * a % P; } return ans; } int ans; vector<int> num; vector<char> op(20); bool dfs(int step = 1, ll cur = num[0]) { if (step == num.size()) return cur == ans; op[step] = '+'; if (dfs(step + 1, cur + num[step])) return true; op[step] = '-'; if (dfs(step + 1, cur - num[step])) return true; if (cur > 0 && num[step] > 0) { op[step] = '#'; if (dfs(step + 1, qpow(cur % num[step], cur, num[step]))) return true; } return false; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); string s; cin >> s; for (int i = 0;i < s.size();i++) { if (isdigit(s[i])) ans = ans * 10 + s[i] - '0'; else num.push_back(ans), ans = 0; } if (dfs()) { cout << num[0]; for (int i = 1;i < num.size();i++) cout << op[i] << num[i]; cout << '=' << ans << '\n'; } else cout << -1 << '\n'; return 0; } 枚举完求值,用到表达式计算。 这里给了一个模板,可以修改map的优先级,支持带括号的二元运算,以及伪负号运算(指负号运算必须打括号)。 #include <bits/stdc++.h> using ll = long long; using namespace std; ll qpow(ll a, ll k, ll P) { ll ans = 1; while (k) { if (k & 1) ans = ans * a % P; k >>= 1; a = a * a % P; } return ans; } map<char, int> mp = { {'+',0},{'-',0},{'#',0},{'=',0} }; bool calc(string s) { vector<ll> num = { 0 }; vector<char> op; for (auto ch : s) { if (ch >= '0' && ch <= '9') num.back() = num.back() * 10 + ch - '0'; else { while (op.size() && mp[ch] <= mp[op.back()]) { char ope = op.back(); op.pop_back(); ll x = num.back(); num.pop_back(); if (ope == '+') num.back() += x; else if (ope == '-') num.back() -= x; else if (ope == '#') { if (x <= 0) return false; num.back() = qpow(num.back() % x, num.back(), x); } } if (ch == '#' && num.back() <= 0) return false; op.push_back(ch); num.push_back(0); } } return num[0] == num[1]; } string s; bool dfs(int step = 0) { if (step == s.size()) return calc(s); if (s[step] == '?') { s[step] = '+'; if (dfs(step + 1)) return true; s[step] = '-'; if (dfs(step + 1)) return true; s[step] = '#'; if (dfs(step + 1)) return true; s[step] = '?'; } else { while (step < s.size() && s[step] != '?') step++; if (dfs(step)) return true; } return false; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); cin >> s; if (dfs()) cout << s << '\n'; else cout << -1 << '\n'; return 0; } H 题解 知识点:概率dp。 可能重要的前置知识(大佬请跳过): 对于一维期望dp,在第 \(i\) 步时,其向其他状态转移的起点只有 \(f_i\) 一种,因此在第 \(i\) 步起点是状态 \(f_i\) 的概率是百分百的,变化的期望直接加就行。 例如,有期望状态 \(f_i\) 。操作有两种,概率分别为 \(\dfrac{1}{4},\dfrac{3}{4}\),两种操作的贡献分别是 \(1,3\) 。那么可以有转移方程: \[f_{i+1} = \dfrac{1}{4}(f_i+1) + \dfrac{3}{4}(f_i+3) \] 但是,如果在一维期望dp的基础上,设每一步都有多个不同的状态,那么转移时的期望就不是简单加法了。 具体的说,在第 \(i\) 步时,其向其他状态转移的起点如果是 \(f_{i,j}\) 有 \(j\) 种,那么显然这 \(j\) 种状态都有概率成为起点,满足概率的总和为百分百。因此考虑 \(f_{i,j}\) 为起点做转移时,变化的期望需要乘上其作为起点的概率,表示这步操作在 \(f_{i,j}\) 作为起点的概率下的期望。当然,期望 \(f_{i,j}\) 本身不需要再乘一遍概率,因为求这个期望时已经考虑了到这个状态的概率,同时我们还可以知道这 \(j\) 种期望的总和就是第 \(i\) 步的总期望。 例如,有期望状态 \(f_{i,0/1/2}\) ,设 \(f_{i,j}\) 的概率为 \(g_{i,j}\) 。操作有两种,概率分别为 \(\dfrac{1}{4},\dfrac{3}{4}\) ,我们假设从 \(f_{i,0/2}\) 都可以通过两种操作转移到 \(f_{i+1,0}\) ,两种操作的贡献对于两种状态分别是 \(1,2\) 和 \(5,6\) 。那么对于 \(f_{i+1,0}\) 可以有转移方程: \[\begin{aligned} f_{i+1,0} &= \dfrac{1}{4}(f_{i,0} + 1\cdot g_{i,0}) + \dfrac{3}{4}(f_{i,0} + 2 \cdot g_{i,0})\\ &+\dfrac{1}{4}(f_{i,2} + 5\cdot g_{i,2})+\dfrac{3}{4}(f_{i,2} + 6 \cdot g_{i,2}) \end{aligned} \] 接下来就可以轻松(真的吗)做这道题了。 设 \(f_{i,j,k}\) 为执行到第 \(i\) 步且满足串首状态为 \(j\) 、串尾状态为 \(k\) 的期望个数。其中,\(j = 0/1\) 表示串首是 red 或 edr , \(k = 0/1\) 同理。 设 \(g_{i,j,k}\) 为对应的 \(f_{i,j,k}\) 发生的概率。注意,除了四种串首尾的状态,还有一种空串的状态,这里没有标记到数组里,但是每步还是得自己手动加上去的,我们记 \(prob\) 为空串的概率,空串的期望为 \(0\) 不需要考虑。 因此有转移方程: \[\left\{ \begin{aligned} f_{i+1,0,0} &= \frac{1}{3} \cdot ((0 + 1 \cdot prob) + (f_{i,0,0} + 1 \cdot g_{i,0,0})+(f_{i,0,1}+1\cdot g_{i,0,1}))\\ &+ \frac{1}{3} \cdot 0\\ &+ \frac{1}{3} \cdot (10 \cdot f_{i,0,0})\\ f_{i+1,0,1} &= \frac{1}{3} \cdot 0\\ &+ \frac{1}{3} \cdot ((f_{i,0,0} + 0 \cdot g_{i,0,0})+(f_{i,0,1}+1 \cdot g_{i,0,1}))\\ &+ \frac{1}{3} \cdot (10\cdot f_{i,0,1})\\ f_{i+1,1,0} &=\frac{1}{3} \cdot ((f_{i,1,0} + 1 \cdot g_{i,1,0})+(f_{i,1,1}+1 \cdot g_{i,1,1}))\\ &+\frac{1}{3} \cdot 0\\ &+\frac{1}{3} \cdot (10 \cdot f_{i,1,0})\\ f_{i+1,1,1} &= \frac{1}{3} \cdot 0\\ &+ \frac{1}{3} \cdot ((0 + 0 \cdot prob) + (f_{i,1,0} + 0 \cdot g_{i,1,0})+(f_{i,1,1}+1 \cdot g_{i,1,1}))\\ &+ \frac{1}{3} \cdot (10 \cdot f_{i,1,1} + 9 \cdot g_{i,1,1})\\ g_{i+1,0,0} &= \frac{1}{3} \cdot (prob + g_{i,0,0} + g_{i,0,1}) + \frac{1}{3} \cdot 0 + \frac{1}{3} \cdot g_{i,0,0}\\\ g_{i+1,0,1} &= \frac{1}{3} \cdot 0 + \frac{1}{3} \cdot (g_{i,0,0} + g_{i,0,1}) + \frac{1}{3} \cdot g_{i,0,1}\\\ g_{i+1,1,0} &= \frac{1}{3} \cdot (g_{i,1,0} + g_{i,1,1}) + \frac{1}{3} \cdot 0 + \frac{1}{3} \cdot g_{i,1,0}\\\ g_{i+1,1,1} &= \frac{1}{3} \cdot 0 + \frac{1}{3} \cdot (prob + g_{i,1,0} + g_{i,1,1}) + \frac{1}{3} \cdot g_{i,1,1}\\\ prob' &= \frac{1}{3} \cdot prob \end{aligned} \right. \] 写的很详细了,三个 \(\dfrac{1}{3}\) 对应三种操作,分别算一下概率和期望转移就行。特别注意,\(f_{i+1,1,1}\) 的操作三转移可以产生十倍加九的期望。 代码用滚动数组压缩了一维, \(f_{j,k,0/1}\) 代表第 \(i\) 步的各种概率/期望, \(g_{j,k,0/1}\) 代表第 \(i+1\) 步的各种概率/期望。并且,代码转移时是用子状态刷表,而非如上述转移方程填表,因为写起来比较方便。填表也能写,本质都是一样的,很好理解。 推荐使用 Modint 不然开 long long 也救不了打 % 打到手酸。 时间复杂度 \(O(k)\) 空间复杂度 \(O(1)\) 代码 #include <bits/stdc++.h> using ll = long long; using namespace std; const int P = 1e9 + 7; struct Modint { int val; Modint(int _val = 0):val(_val %P) { format(); } Modint(ll _val):val(_val %P) { format(); } //if val in [-P,2P) //maybe slower than global version Modint &format() { if (val < 0) val += P; if (val >= P) val -= P; return *this; } Modint inv()const { return qpow(*this, P - 2); } Modint &operator+=(const Modint &x) { val += x.val;return format(); } Modint &operator-=(const Modint &x) { val -= x.val;return format(); } Modint &operator*=(const Modint &x) { val = 1LL * val * x.val % P;return *this; } Modint &operator/=(const Modint &x) { return *this *= x.inv(); } friend Modint operator-(const Modint &x) { return { -x.val }; } friend Modint operator+(Modint a, const Modint &b) { return a += b; } friend Modint operator-(Modint a, const Modint &b) { return a -= b; } friend Modint operator*(Modint a, const Modint &b) { return a *= b; } friend Modint operator/(Modint a, const Modint &b) { return a /= b; } friend Modint qpow(Modint a, ll k) { Modint ans = 1; while (k) { if (k & 1) ans = ans * a; k >>= 1; a = a * a; } return ans; } friend istream &operator>>(istream &is, Modint &x) { ll _x; is >> _x; x = { _x }; return is; } friend ostream &operator<<(ostream &os, const Modint &x) { return os << x.val; } }; /* f[0/1][0/1][0]:概率 f[0/1][0/1][1]:期望 00 red-red 01 red-edr 10 edr-red 11 edr-edr 注意还有一种空串情况 需要每次操作前手动加 */ int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int k; cin >> k; array<array<array<Modint, 2>, 2>, 2> f = {}; Modint inv3 = Modint(3).inv(); Modint prob = 1; for (int i = 1;i <= k;i++) { //prod指空串概率,空串期望始终为0不需要管 array<array<array<Modint, 2>, 2>, 2> g = {}; g[0][0][0] = inv3 * prob; g[1][1][0] = inv3 * prob; g[0][0][1] = inv3 * prob; //空串到g[1][1]的期望还是0,不用管 for (auto i : { 0,1 }) { for (auto j : { 0,1 }) { g[i][0][0] += inv3 * f[i][j][0];//操作1后的概率 g[i][1][0] += inv3 * f[i][j][0];//操作2后的概率 g[i][j][0] += inv3 * f[i][j][0];//操作3后的概率 g[i][0][1] += inv3 * f[i][j][1];//操作1后的期望 g[i][0][1] += inv3 * f[i][j][0]; g[i][1][1] += inv3 * f[i][j][1];//操作2后的期望 if (j == 1) g[i][1][1] += inv3 * f[i][j][0];//只有?1才能加 g[i][j][1] += 10 * inv3 * f[i][j][1];//操作3后的期望 if (i == 1 && j == 1) g[i][j][1] += 9 * inv3 * f[i][j][0];//只有11能加9个 } } prob *= inv3; f = g; } Modint sum = 0; for (auto i : { 0,1 })for (auto j : { 0,1 }) sum += f[i][j][1]; cout << sum << '\n'; return 0; } I 题解 知识点:数论,构造。 偶数情况,若 \(x-1\) 是素数构造 \(n = (x-1)^2\) ,则 \(f(n) = 1+x-1=x\) ; 若 \(x-3\) 是素数构造 \(n = 2(x-3)\) ,则 \(f(n) = 1+2+x-3=x\) 。 奇数情况,因为一定存在 \(1\) 因子,我们考虑使其他因子的和凑出一个偶数 \(x-1\) 。考虑最简单的素数情况,因为哥德巴赫猜想,一个大于等于 \(4\) 的偶数可以被分解成两个素数之和,我们只要找到两个不同的素数 \(p,q\) 使得 \(p+q = x-1\) ,那么构造 \(n = pq\) ,则 \(f(n) = 1+p+q=x\) 。 注意,哥德巴赫猜想所述是两个素数之和,不是两个不同的素数。经过测试 int 范围内,大于等于 \(8\) 的偶数都可以被分解为两个不同的素数之和,因此大于等于 \(9\) 的奇数我们无脑无解即可。 我们需要特判 \(x = 1,3,7\) ,因为这些情况确实有解,但不能通过哥德巴赫猜想构造。 时间复杂度 \(O(x)\) 空间复杂度 \(O(x)\) 代码 #include <bits/stdc++.h> using namespace std; using ll = long long; const int N = 1e6 + 7; bool vis[N]; vector<int> prime; void get_prime(int n) { for (int i = 2;i <= n;i++) { if (!vis[i]) prime.push_back(i); for (int j = 0;j < prime.size() && i * prime[j] <= n;j++) { vis[i * prime[j]] = 1; if (!(i % prime[j])) break; } } } bool solve() { int x; cin >> x; if (x == 1) { cout << 2 << '\n'; return true; } if (x == 3) { cout << 4 << '\n'; return true; } if (x == 7) { cout << 8 << '\n'; return true; } if (x & 1) { for (int i = 0;i < prime.size() && 2 * prime[i] < x - 1;i++) { if (!vis[x - 1 - prime[i]]) { cout << 1LL * prime[i] * (x - 1 - prime[i]) << '\n'; return true; } } } else { if (!vis[x - 1]) cout << 1LL * (x - 1) * (x - 1) << '\n'; else cout << 1LL * 2 * (x - 3) << '\n'; return true; } return false; } int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int t = 1; cin >> t; get_prime(1e6); while (t--) { if (!solve()) cout << -1 << '\n'; } return 0; } K 题解 知识点:贪心,数学。 给你前 \(n\) 种素数,每个素数有 \(a_i\) 个。 设 \(size = \sum_{i=1}^n a_i\) 。现在将这 \(size\) 个素数排成一个序列,设 \(f(i)\) 为序列中 \([1,i]\) 的数的乘积的因子的数量。现在求 \([1,size]\) 的 \(f(i)\) 的和,即 \(\sum_{i=1}^{size} f(i)\) ,的最大值。 显然,我们需要尽可能让前面的 \(f(i)\) 的越大越好。我们知道,乘积的因子数量等于各个素数个数加 \(1\) 的乘积,通过一些尝试很容易发现均摊素数的个数,比连续安排同一种素数得到的结果要大很多,因此我们每次安排还能安排的素数中出现次数最小的那个素数。 我们设 \(cnt_i\) 表示出现至少 \(i\) 次的素数个数,我们发现 \(f(i)\) 的结果呈现 \(2,2^2,\cdots,2^{cnt_1},2^{cnt_1-1} \cdot 3,\cdots,2^{cnt_1-cnt_2} \cdot 3^{cnt_2},\cdots\) 。直接求和要加 \(size\) 次是不可行的,因此我们先用差分维护好 \(cnt_i\) ,随后用等比公式对每 \(cnt_i\) 个数直接求和。 设 \(pre\) 为 \(cnt_{i-1}\) 段的最后一个数字,那么 \(cnt_i\) 段的总和为 \(pre \cdot \dfrac{1-\frac{i+1}{i}^{cnt_i}}{1-\frac{i+1}{i}}\) ,最后一个数字 \(pre' = pre \cdot \dfrac{i+1}{i}^{cnt_i}\) ,于是就可以递推求和了。 时间复杂度 \(O(2 \cdot 10^5 + n)\) 空间复杂度 \(O(2 \cdot 10^5)\) 代码 #include <bits/stdc++.h> using ll = long long; using namespace std; const int P = 1e9 + 7; ll qpow(ll a, ll k) { ll ans = 1; while (k) { if (k & 1) ans = ans * a % P; k >>= 1; a = a * a % P; } return ans; } ll inv(ll a) { return qpow(a, P - 2); } int cnt[200007]; int main() { std::ios::sync_with_stdio(0), cin.tie(0), cout.tie(0); int n; cin >> n; for (int i = 1;i <= n;i++) { int x; cin >> x; cnt[1]; cnt[x + 1]--; } for (int i = 1;i <= 2e5;i) cnt[i] += cnt[i - 1]; int pre = 1; int ans = 0; for (int i = 1;i <= 2e5;i++) { int f = 1LL * (i + 1) * inv(i) % P; int g = qpow(f, cnt[i]); ans = (ans + 1LL * pre * f % P * (1 - g + P) % P * inv(1 - f + P) % P) % P; pre = 1LL * pre * g % P; } cout << ans << '\n'; return 0; }
2024-01-18
324
0
0
网络聚合
2024-01-18
架构设计(九):估算
架构设计(九):估算 作者:Grey 原文地址: 博客园:架构设计(九):估算 CSDN:架构设计(九):估算 估算在系统设计中非常重要,这决定了你的设计是否可以满足要求,要实现比较靠谱的估算,就需要对如下几个概念熟练掌握 第一个概念:二的幂 尽管在处理分布式系统时,数据量可能是巨大的,但计算都可以归结为基础知识。为了获得正确的计算结果,关键是要知道使用2的幂的数据量单位。一个字节是一个8位的序列。一个ASCII字符使用一个字节的内存(8位)。可参考如下表格 次幂 近似值 名称 10 1000 1KB 20 100万 1MB 30 10亿 1GB 40 1万亿 1TB 50 1千万亿 1PB 第二个概念:关于延时指标的常见场景 注:以下指标说明来自J. Dean.Google Pro Tip: Use Back-Of-The-Envelope-Calculations To Choose The Best Design ,虽然是基于2010年的状况,但是目前这些指标还是有一定的参考价值。 操作 参考时间量级 L1高速缓存 0.5 ns 分支错误预测 5 ns L2高速缓存 7 ns 互斥器锁定/解锁 100 ns 主内存 100 ns 用Zippy压缩1K字节 10,000 ns 通过1 Gbps网络发送2K字节 20,000 ns 从内存中连续读取1MB 250,000 ns 在同一数据中心内的数据往返 500,000 ns 磁盘搜索 10,000,000 ns 从网络中连续读取1MB 10,000,000 ns 从磁盘顺序读取1MB 30,000,000 ns 上述场景也有一个可视化的工具可以查看,见Latency Numbers Every Programmer Should Know 目前展示到了 2020 年。 通过分析上述数字,可以得到以下结论。 内存很快,但磁盘很慢。 如果可能的话,要避免磁盘寻道。 简单的压缩算法是快速的。 如果可能的话,在通过互联网发送数据之前要进行压缩。 数据中心通常在不同地区,在它们之间发送数据需要时间 第三个需要了解的概念是:系统可用时间百分比 高可用性是指一个系统在一个理想的长时间内持续运行的能力。高可用性是以百分比来衡量的,100%意味着一个服务没有停机时间。大多数服务在99%和100%之间。服务水平协议(SLA)是服务提供者的一个常用术语。这是你(服务提供商)和你的客户之间的协议,这个协议正式定义了你的服务将提供的正常运行时间水平,正常运行时间传统上是以九为单位衡量。九位数越多,越好。如表2-3所示,九位数与预期的系统停机时间相关。 可用性 平均每天停机时间 平均每年停机时间 99% 14.40分钟 3.65天 99.9% 1.44分钟 8.77小时 99.99% 8.64秒 52.6分钟 99.999% 864毫秒 5.26分钟 99.9999% 86.4毫秒 31.56秒 云供应商亚马逊、谷歌和微软将其SLA设定为99.9%或以上。 参考资料 System Design Interview
2024-01-18
215
0
0
网络聚合
2024-01-18
prometheus-添加监控linux服务器
1. prometheus-添加监控linux服务器 prometheus添加监控linux服务器 node_exporter:用于监控Linux系统的指标采集器。 常用指标: CPU 内存 硬盘 网络流量 文件描述符 系统负载 系统服务 数据接口:http://IP:9100 使用文档:https://prometheus.io/docs/guides/node-exporter/ GitHub:https://github.com/prometheus/node_exporter 安装部署 下载node_exporter包 https://github.com/prometheus/node_exporter/releases/download/v1.1.2/node_exporter-1.1.2.linux-amd64.tar.gz 监控端主机下载 [root@VM-0-17-centos ~]# wget https://github.com/prometheus/node_exporter/releases/download/v1.1.2/node_exporter-1.1.2.linux-amd64.tar.gz 解压 [root@VM-0-17-centos ~]# tar -xvf node_exporter-1.1.2.linux-amd64.tar.gz node_exporter-1.1.2.linux-amd64/ node_exporter-1.1.2.linux-amd64/LICENSE node_exporter-1.1.2.linux-amd64/NOTICE node_exporter-1.1.2.linux-amd64/node_exporter 拷贝到opt目录下存放 [root@VM-0-17-centos ~]# mv node_exporter-1.1.2.linux-amd64 /opt/ [root@VM-0-17-centos ~]# ll /opt/node_exporter-1.1.2.linux-amd64/ total 18748 -rw-r--r-- 1 3434 3434 11357 Mar 5 17:41 LICENSE -rwxr-xr-x 1 3434 3434 19178528 Mar 5 17:29 node_exporter -rw-r--r-- 1 3434 3434 463 Mar 5 17:41 NOTICE [root@VM-0-17-centos ~]# mv /opt/node_exporter-1.1.2.linux-amd64 /opt/node_exporter 切换目录 [root@VM-0-17-centos ~]# cd /opt/node_exporter/ [root@VM-0-17-centos node_exporter]# ll total 18748 -rw-r--r-- 1 3434 3434 11357 Mar 5 17:41 LICENSE -rwxr-xr-x 1 3434 3434 19178528 Mar 5 17:29 node_exporter -rw-r--r-- 1 3434 3434 463 Mar 5 17:41 NOTICE 启动服务尝试 [root@VM-0-17-centos node_exporter]# ./node_exporter level=info ts=2021-05-19T07:00:05.583Z caller=node_exporter.go:178 msg="Starting node_exporter" version="(version=1.1.2, branch=HEAD, revision=b597c1244d7bef49e6f3359c87a56dd7707f6719)" level=info ts=2021-05-19T07:00:05.583Z caller=node_exporter.go:179 msg="Build context" build_context="(go=go1.15.8, user=root@f07de8ca602a, date=20210305-09:29:10)" level=warn ts=2021-05-19T07:00:05.583Z caller=node_exporter.go:181 msg="Node Exporter is running as root user. This exporter is designed to run as unpriviledged user, root is not required." level=info ts=2021-05-19T07:00:05.583Z caller=filesystem_common.go:74 collector=filesystem msg="Parsed flag --collector.filesystem.ignored-mount-points" flag=^/(dev|proc|sys|var/lib/docker/.+)($|/) level=info ts=2021-05-19T07:00:05.583Z caller=filesystem_common.go:76 collector=filesystem msg="Parsed flag --collector.filesystem.ignored-fs-types" flag=^(autofs|binfmt_misc|bpf|cgroup2?|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|iso9660|mqueue|nsfs|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|selinuxfs|squashfs|sysfs|tracefs)$ level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:106 msg="Enabled collectors" level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=arp level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=bcache level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=bonding level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=btrfs level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=conntrack level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=cpu level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=cpufreq level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=diskstats level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=edac level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=entropy level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=fibrechannel level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=filefd level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=filesystem level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=hwmon level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=infiniband level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=ipvs level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=loadavg level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=mdadm level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=meminfo level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=netclass level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=netdev level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=netstat level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=nfs level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=nfsd level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=powersupplyclass level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=pressure level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=rapl level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=schedstat level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=sockstat level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=softnet level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=stat level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=textfile level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=thermal_zone level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=time level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=timex level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=udp_queues level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=uname level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=vmstat level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=xfs level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:113 collector=zfs level=info ts=2021-05-19T07:00:05.584Z caller=node_exporter.go:195 msg="Listening on" address=:9100 level=info ts=2021-05-19T07:00:05.584Z caller=tls_config.go:191 msg="TLS is disabled." http2=false 配置为系统服务管理 编写系统配置服务 [root@iZj6cbgktk3zjpge312vq1Z node_exporter]# vim /usr/lib/systemd/system/node_exporter.service [root@iZj6cbgktk3zjpge312vq1Z node_exporter]# cat /usr/lib/systemd/system/node_exporter.service [Unit] Description=node_exporter [Service] # 添加认证密码文件/opt/node_exporter/config.yml ,默认可以不需要 ExecStart=/opt/node_exporter/node_exporter --web.config=/opt/node_exporter/config.yml ExecReload=/bin/kill -HUP $MAINPID KillMode=process Restart=on-failure [Install] WantedBy=multi-user.target 添加config配置文件 [root@VM-0-17-centos node_exporter]# yum install httpd-tools –y [root@VM-0-17-centos node_exporter]# htpasswd -nBC 12 '' | tr -d ':\n' New password: #这里输入的123456 Re-type new password: #这里输入的123456 $2y$12$.YGKNPkYfSOsm.JataWRUe4vWdTS8nW6YtPQI0Jr14eTv6E5Fpdga # 这段是生成的key 编写启动配置文件 [root@iZj6cbgktk3zjpge312vq1Z node_exporter]# vim config.yml [root@iZj6cbgktk3zjpge312vq1Z node_exporter]# cat config.yml basic_auth_users: prometheus: $2y$12$.YGKNPkYfSOsm.JataWRUe4vWdTS8nW6YtPQI0Jr14eTv6E5Fpdga 启动服务 systemctl daemon-reload systemctl start node_exporter systemctl enable node_exporter 在prometheus添加主机 添加配置文件 [root@iZj6cbgktk3zjpge312vq2Z prometheus]# vim prometheus.yml [root@iZj6cbgktk3zjpge312vq2Z prometheus]# cat prometheus.yml # my global config global: scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. # scrape_timeout is set to the global default (10s). Alertmanager configuration alerting: alertmanagers: static_configs: targets: - alertmanager:9093 Load rules once and periodically evaluate them according to the global 'evaluation_interval'. rule_files: - "first_rules.yml" - "second_rules.yml" A scrape configuration containing exactly one endpoint to scrape: Here it's Prometheus itself. scrape_configs: The job name is added as a label job=<job_name> to any timeseries scraped from this config. job_name: 'prometheus' metrics_path defaults to '/metrics' scheme defaults to 'http'. static_configs: targets: ['127.0.0.1:9090'] job_name: linux 添加监控主机的用户和密码 basic_auth: username: prometheus password: 123456 static_configs: targets: ['10.1.2.211:9100'] labels: prod: web 重启服务 [root@iZj6cbgktk3zjpge312vq2Z prometheus]# /bin/systemctl restart prometheus 在Prometheus配置文件添加被监控端: 验证prometheus配置文件 验证发现已经有数据了 使用Grafana展示node_exporter数据指标,仪表盘ID:9276 导入仪表盘 添加
2024-01-18
267
0
0
网络聚合
2024-01-18
拥抱下一代前端工具链-Vue老项目迁移Vite探索
作者:京东物流 邓道远 背景描述 随着项目的不断维护,代码越来越多,项目越来越大。调试代码的过程就变得极其痛苦,等待项目启动的时间也越来越长,尤其是需要处理紧急问题的时候,切换项目启动,等待的时间就会显得尤为的漫长。无法忍受这种开发效率的我,决定将老项目迁移至vite。 距离Vite工具发布到现在已经有了一些日子了,工具链与生态已经趋于稳定,最新版本已经更新到了3.0,既然念头已起,心动不如行动。 1、什么是Vite vite 发音为/vit/ 法语中就是快的意思,“人”如其名,就是快 一个开发服务器,它基于原生ES模块,提供了丰富的内建功能,如速度快到惊人的模块热更新(HRM) 一套构建指令,它使用rollop来打包你的代码,并且是预配置的,可输出用于生产环境的高度优化过的静态资源。 2、为什么快 众所周知,当冷启动服务器时,基于打包器的启动必须优先抓取并构建你的整个应用,然后才能提供服务,这一抓取构建的过程随着文件越来越多,时间也会越来越长。 而Vite却通过将应用中的木块区分为依赖和源码两类,从而优化了大量的服务器启动时间。 依赖大多为在开发时不会变动的纯 JavaScript。一些较大的依赖(例如有上百个模块的组件库)处理的代价也很高。依赖也通常会存在多种模块化格式(例如 ESM 或者 CommonJS)。 Vite 将会使用 esbuild预构建依赖。esbuild 使用 Go 编写,并且比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。 源码通常包含一些并非直接是 JavaScript 的文件,需要转换(例如 JSX,CSS 或者 Vue/Svelte 组件),时常会被编辑。同时,并不是所有的源码都需要同时被加载(例如基于路由拆分的代码模块)。 Vite 以原生 ESM方式提供源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。 3、如何完成老项目迁移 当前项目是Vue2.0,vue-cli4.0,node v14.18.2 3.1 首先我们需要先明确项目结构 与原来的Vue老项目相比,模板文件 index.html 需要从public挪到项目根目录中,Vite将 index.html 视为源码和模块图的一部分。由于我们只有一个入口文件,所以在index.html中需要引入main.ts <script type="module" src="/src/main.ts"></script> 而且运行过程中可能会遇到下面写法引发的报错 <link rel="icon" href="<%= BASE_URL %>favicon.ico" /> [vite] Internal server error: URI malforme 解决办法是可以写一个简单的插件替换一下 res = code.replace(/<%=\s+BASE_URL\s+%>/g, baseDir); 与Vue-cli相同,需要一个配置文件 vite.cofnig.js, 与原来的vue.config.js同级 3.2 安装依赖 既然我们使用Vite,那么我们需要安装一个vite依赖。但是我们的老项目是Vue2.0,vite优先支持Vue3.0,所以我们还需要一个转换工具 "vite-plugin-vue2" npm i vite vite-plugin-vue2 -S 3.3 修改配置文件 修改package.json中的scripts,启动和打包方式使用vite "serve": "vite", "build": "vite build", 修改vite.config.js,与vue.config.js相似 import { defineConfig } from 'vite' import { createVuePlugin } from 'vite-plugin-vue2' // https://vitejs.dev/config/ 这一行可以增加编辑器代码提示 export default defineConfig({ plugins: [ createVuePlugin({ jsx: true, // 兼容项目中的jsx组件 vueTemplateOptions: {} }), ], resolve: { extensions: ['.vue', '.js', '.ts', '.jsx', '.tsx', '.json'], alias: [ { find: '@', replacement: '/src' } ] }, server: { open: true, // 控制台直接打开浏览器 host: 'xxxx.jd.com', // 本地host allowedHosts: ['.jd.com', '.jdwl.com', '.jd.co.th', '.jd.id'], port: 80, cors: true, proxy: { '/api': { target: 'https://xxx.jd.com', changeOrigin: true, rewrite: path => path.replace(/^/api/, '/api') } } }, }) 3.4 剔除原来的webpack相关依赖 可以手动剔除 也可以重新启动一个vite项目再将所需代码移动到vite项目中 3.5 启动应用 这个时候我们就可以启动应用了,不出意外的话,会有许多的报错信息,不过不要慌,我们一个一个的解决 4、遇见的问题汇总 4.1 环境变量 webpackl里的环境变量是默认存储在process.env里的,而vite是存储在import.meta.env里的 import.meta.env.MODE: {string} 应用运行的模式。 import.meta.env.BASE_URL: {string} 部署应用时的基本 URL。他由base 配置项决定。 import.meta.env.PROD: {boolean} 应用是否运行在生产环境。 import.meta.env.DEV: {boolean} 应用是否运行在开发环境 (永远与 import.meta.env.PROD相反)。 当然,既然是老项目,这种调用位置会有很多,我们可以使用比较简单的做法来兼容 export default defineConfig({ define: { 'process.env': {} }, }) 4.2 global 变量 因为VIte 是 ESM机制,有些包内部使用了 node 的 global对象,解决此问题可以通过自建pollfill, 然后在main.ts顶部引入 // polyfills if (typeof (window as any).global === 'undefined') { ;(window as any).global = window } 4.3 Scss全局变量报错 这一点是vite与vue-cli 配置方式不同引发,而且如果使用了环境变量也需要适配vite的写法兼容 export default defineConfig({ css: { preprocessorOptions: { scss: { additionalData: '$ossHostVariable: \'import.meta\u200b.env.VUE_APP_OSS_HOST\';' } } } }) 4.4 path 报错 Vite 是 ESM机制 path是node的包,所以需要兼容浏览器的引入方式,需要安装依赖 “path-broswserfiy” 只需要将引入的包替换即可 import path from 'path' // 替换成 import path from 'path-broswserfiy' 4.5 Require报错 问题的引发与上面一致 都是模块加载方式的不同导致的,可以通过" vite-plugin-require-transform"插件来解决 import requireTransform from 'vite-plugin-require-transform' export default defineConfig({ plugins: [ requireTransform({}) ] }) 4.6 vue组件的动态导入 vue的组件导入方式有很多,vite可以支持 () => import('/.vue')的方式导入,不过与webpack的区别在于需要补全文件的后缀,动态导入需要 import.meta.glob的方式 const load = import.meta.glob('@/views/**/index.vue'); export const constantRoutes: any = [ { path: '/404', component: load['404'] }, ] 4.7 编译时的分包策略 const SPLIT_CHUNK_CONFIG = [ { match: /[\\/]src[\\/]_?common(.*)/, output: 'chunk-common', }, { match: /[\\/]src[\\/]_?component(.*)/, output: 'chunk-component', }, ]; const rollupOptions = { output: { chunkFileNames: 'assets/js/[name]-[hash].js', entryFileNames: 'assets/js/[name]-[hash].js', assetFileNames: 'assets/static/[name]-[hash].[ext]', manualChunks(id) { for (const item of SPLIT_CHUNK_CONFIG) { const { match, output } = item; if (match.test(id)) { return output; } } if (id.includes('node_modules')) { return id.toString().split('node_modules/')[1].split('/')[0].toString(); } }, }, } 5、启动时间 不多说了 上图 不过还会有一些问题,开发模式下比如页面首次加载时间比较缓慢,大约在5s左右,不过这也是可以理解的,毕竟编译过程都交给了浏览器,相比于老项目冷启动动辄2 3分钟的体验,已经是天大的提升了。 6、总结 最后再来回顾一下,整体的迁移过程。 首先,明确项目结构,index.html模板文件 提到根目录下,统计增加vite.config.js文件。 然后,编写配置文件 vite.config.js 注意与 vue.config.js上的语法区别,注意兼容写法。 最后,处理项目中两种打包工具的不兼容写法。大部分还是模块规范的区别,node环境的变量以及语法所引发,可以通过各种各样的插件来兼容解决。 以上即为本次迁移的全部过程,丰富、优化了前端工具链的构建流程,极大的提升了开发人员的幸福感,以及开发体验,项目冷启动时间更是提升了百分之99%。虽然前期遇到了许多的坑,但是成功后的感受就是一个字,"真香"。
2024-01-18
205
0
0
网络聚合
2024-01-18
ASP.NET Core - 依赖注入(二)
.NET Core 依赖注入的基本用法 话接上篇,这一章介绍 .NET Core 框架自带的轻量级 Ioc 容器下服务使用的一些知识点,大家可以先看看上一篇文章 [ASP.NET Core - 依赖注入(一)] 2.3 服务解析 通过 IServiceCollection 注册了服务之后,可以通过以下方式解析相应服务的实例: IServiceProvider IServiceProiver 实例由 IServiceCollection 通过 BuildServiceProvider() 方法创建,在 ASP.NET Core 中,主机启动的时候会创建一个全局的 IServiceProvider,并且此实例也在容器当中。所有在容器注册过的服务都可以通过 IServiceProiver 进行解析,当然该服务的依赖项必须也在容器中注册。 ActivatorUtilities 用于手动创建未在DI容器中注册的服务实例 2.3.1 服务注入方式 当我们通过容器解析一个服务实例的时候,容器根据当前服务的链式依赖关系图解析其依赖项,根据依赖项的生命周期或创建、或从已有的实例获取,然后注入到我们解析的服务当中。在一个服务中获取另一个服务实例的方式由以下几种: (1) 构造函数注入 构造函数注入是非常常见的服务注入方式,也是微软最推荐的方式,这种方式可以明确地声明当前类所依赖的东西,一目了然。如同上面的示例代码中,使用的就是构造函数注入方式。构造函数注入,对于类的构造函数有以下要求: 构造函数可以接收非依赖注入的参数,但必须提供默认值 当服务通过 IServiceProvider 解析时,要求构造函数必须是 public 当服务由 ActivatorUtilities 解析时,构造函数注入要求只存在一个适用的构造函数。 支持构造函数重载,但其参数可以全部通过依赖注入来实现的重载只能存在一个。 如果发现构造函数时存在歧义,将引发异常,例如以下情况: public class ExampleService { public ExampleService() { } public ExampleService(ILogger<ExampleService> logger) { // omitted for brevity } public ExampleService(IOptions<ExampleOptions> options) { // omitted for brevity } } (2) 属性注入 这里有一点需要说明,.NET Core 内置的依赖注入框架并不支持属性注入,如果需要使用属性注入需要结合第三方依赖注入框架进行使用,如autofac。 顾名思义,属性注入就是通过类中的属性注入需要的服务,要求属性必须是 public ,并且具备 get、set 访问器。如下: 属性注入一般用于注入一些即使缺失了也不会导致当前类无法工作的依赖项,如日志记录等。这种时候会为数据注入设置一个默认实现,防止该属性为空,导致当前类的功能受影响。 (3) 方法注入 通过 FromServicesAttribute 特性在控制器的方法参数中注入,这种方式只能用于控制器。默认情况下,控制器示例由容器来管理,在入口文件调用 builder.Services.AddControllers(); 时注册到容器中。 [HttpGet(nameof(InjectTest3))] public Task InjectTest3([FromServices] IRabbit rabbit) { Console.WriteLine(rabbit is Rabbit); return Task.CompletedTask; } 这种方式用于缩小依赖注入的粒度,适用于注入的服务只在当前方法使用的时候,是对构造函数注入的简化。 (4) 手动解析 在.NET框架中,任何可以拿得到 IServiceProvider 实例的地方都可以通过 GetRequiredService() 或者 GetService() 解析我们需要的服务。直接使用 IServiceProvider 是服务定位器模式的一个示例。这通常被认为是反模式,因为它隐藏了类的依赖关系。这种方式在某些情况下是有用的,但是应该尽量避免。 GetService() 与 GetRequiredService() 的区别在于解析服务时,如果该服务没有在容器中注册,前者会返回Null,而后者会抛出异常。两者的区别可参考以下文件:ASP.NET Core中GetService()和GetRequiredService()之间的区别 除了通过注入 IServiceProvider 来解析服务之外,其他的方式,例如 HttpContext 中也包含 IServiceProvider 实例,如: var rabbit1 = HttpContext.RequestServices.GetRequiredService<IRabbit>(); 2.3.2 ActivatorUtilities 使用 通过 ActivatorUtilities 解析服务比较简单,常用的由以下两个方法: ActivatorUtilities.CreateInstance<HelloService>(provider, "test"); ActivatorUtilities.GetServiceOrCreateInstance<IHelloService>(provider); 其中 CreateInstance 方法的泛型类型需要是具体的类型,而不是接口,这个方法还可以传入构造函数需要的,但没有在容器中注册的参数。GetServiceOrCreateInstance 方法会先尝试从容器获取实例,获取不到再创建,不支持不在容器中注册的构造函数参数。 参考文章: ASP.NET Core 依赖注入 | Microsoft Learn 理解ASP.NET Core - 依赖注入(Dependency Injection) ASP.NET Core 系列: 目录:ASP.NET Core 系列总结 上一篇:ASP.NET Core - 依赖注入(一) 下一篇:ASP.NET Core - 依赖注入(三)
2024-01-18
366
0
0
网络聚合
68
69
70
71
72