feat: rewrite about page for 2026. (#21)
Some checks failed
Build blog docker image / Build-Blog-Image (push) Failing after 14s

Signed-off-by: jackfiled <xcrenchangjun@outlook.com>
Reviewed-on: #21
This commit is contained in:
2026-03-03 09:09:49 +00:00
parent 6ea14b186a
commit 462fbb28ac
386 changed files with 1258 additions and 473 deletions

0
source/posts/.gitkeep Normal file
View File

View File

@@ -0,0 +1,48 @@
---
title: 2021年终总结
date: 2022-01-12T16:27:19.0000000
tags:
- 杂谈
- 年终总结
---
2021年已经过去2022年已经来临。每每一年开始的时候我都会展开一张纸或者新建一个文档思量着又是一年时光也该同诸大杂志一般写几句意味深长的话语怀念过去的时光也祝福未来的自己。可往往脑海中已是三万字的长篇落在笔头却又是一个字都没有了。
如今跨年的时候已经过去朋友圈中已经不见文案的踪影我也该重新提笔细说自己2021年中做过的种种。
<!--more-->
## 高考,落下帷幕
在未来我回想2021时想到的第一个念头必然是“高考”都说高考时人生中最重要的分水岭这次考试不仅把我送进了911大学——北京邮电大学也标志着一个时代的结束。
## MY中学杂记
仍然记得,在高中的最后一个寒假,高考前最后一个比较长的假期,我还是没有能够提前写完作业,把有限的时间投入无限的复习大业中去。仍然是到了假期的最后一天再来补作业。
仍然记得,在高考的理综结束之后,我差点跪在铭志楼的楼梯上。
仍然记得,在那天的中午,我们几乎没有吃饭,寝室里出人意料的安静。
仍然记得,在最后的宴会上,我们共同举杯,祝福我们的未来。
## 暑假,说好的狂欢?
在前12年的学生生涯中我们都在期待着这一次的暑假以为在这个没有作业的假期里我们就可以充分的享受人间的美好。可是当时我们不知道这人间的烦恼可不止作业这一种无论是突如其来的疫情导致开学延期还是等待录取时的不安。
虽说在暑假时,拥有了自己的笔记本电脑,可是在高中三年屯下的游戏还是没有玩几个,看来我也是“喜加一”的受害者。虽然在高考后入坑了原神,但是假期间我并没有太过投入的玩。
暑假下定决心要好好的学一学可是看着我gitee上暑假期间那稀疏的提交我就知道我又摸了一个暑假的鱼。
![gitee贡献](./2021-final/1.webp)
即使我想写的很多项目都没有被扎实的推进下来但是学习的一些的C语言还是让我受益匪浅。
现在看来,这个假期真是,**学也没有学好,耍也没有耍好**的典型。
## BUPT——新的开始
尽管有着烦人的疫情我们还是在2021年9月8日顺利走进了北邮的校园。
~~虽然校园很小,但是~~校园第一眼给人的印象还是蛮大的。走在贯穿东西的主干道上,我们逐渐地熟悉校园中的建筑物。
在刚开学的时候北邮放假永不调休的制度着实让我们大吃一惊毕竟连着中秋加国庆加周末几乎放掉了半个月这难道是一般人能承受的吗更不要说我们这群刚刚放了三个月的暑假已经不知道学习是什么东西的freshman了。
刚刚享受玩国庆假期的我们,就又被疫情坑了一把,半个学期就这样被我们混了过去,除了把半个寝室都带进了原神的坑里,其他的我啥也没干。
紧接着就是噩梦一样的体测+半期考试+秋之韵三板斧,这难道是一般人能承受的吗?~~反复以表示强调~~
再然后就是期末考试提前,高数与线代的课时数严重不足,其他几科也是组织完期中考试就准备期末考试,总之就是这个学期没有下半期这个说法了。
这个学期就结束了。
**总结:啥也没干**
## 说说技术
在北邮的一个学期虽然学到的的新东西不多但是自己C语言的能力是有了切实的提高。在大学之前也很少接触这种OJ平台的代码训练这个学期也算是涨了见识。
这个学期说好精进一下自己Python相关的技术结果这个学期Python是我写的最少的一门语言。这个学期反而接触了许多新鲜的语言比如JavaC#Javascript等等。自己也租用了阿里云的云服务器在上面部署了自己的博客和一些小工具。除了语言方面的接触这个学期对于智能硬件的接触也算是比较对针对选修课“基于Arduino的开源手机设计”熟悉了Arduino编程控制相关的知识自己也购买了一套Arduino开发板打算下学期做点有意思的小工作。
在学院的综合影视部干了一年我也算是接触了直播的相关流程对需要的技术栈有了一个初步的了解。还研究过V家编辑器感觉自己没用技能又增加了。
## 总结
2021年就是虽然没有什么大的收获但一些小小的进步总让我们确信未来会更好。

BIN
source/posts/2021-final/1.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,77 @@
---
title: 2022年终总结
date: 2022-12-30T14:58:12.0000000
tags:
- 杂谈
- 年终总结
---
2022是困难的一年。我们需要为2023年做好准备。
<!--more-->
## 总结
尽管人们在撰写年终总结时习惯于”美化“过去一年的境遇但是2022年的情况让我甚至很难用”砥砺奋进”或者“破浪前行”之类的词来形容。
导致2022年这种情况的原因很复杂国际大气候的惊涛骇浪和国内小气候的暗流涌动、新型冠状病毒疫情的反复延续以及我个人的部分特殊情况都能算在其中。
2022年绝对是见证历史的一年而且是大部分人不想见证的历史。2022年2月俄乌冲突爆发7月前日本首相安倍晋三被刺杀8月佩洛西窜访台湾12月巴尔干半岛剑拔弩张。而新型冠状病毒肺炎的防控遭遇重大挫折我们离回到正常的生活更是遥遥无期。“百年未有之大变局”这个提法在2017年末的时候出现在那个时候可能只有少数人明白啥叫“大变局”如今5年过去了可能大家都明白了什么叫做“大变局”。
> 鉴证不是为了过过嘴瘾,而是为了更好的指导自己的生产生活实际。
### 时长八个月的寒假
2021年末12月新冠Omicron变异株在南非首次被检出随即得到命名。在那个时间点上我还不如现在这般关注新变异株的出现和提交也不会知道这个变异株会直接摧毁我在2022年上半年的生活。2022年1月3日我离开学校回到家中而我下一次回到学校就要到8月27日了。
在这8个月的前半段似乎还一切正常。首先是应付因为冬奥会而改成线上的期末考试说来也是奇妙进入大学已经3个学期了我还没有在线下参加过期末考试。然后就开始宅在家的寒假过完一个平淡的年。随着年味散去就开始期待开学的日期但是等来的却是一次次的推迟通知——吉林疫情上海疫情北京疫情随着疫情在一个个地点爆发我们终于意识到自己大学的第二个学期只能完全在家里度过了。
不过讲道理,在家里学习和在学校里学习的孰优孰劣是一个值得探讨的问题。我个人认为,还是在学校里更加的适合学习。学校可以为你提供生活中所需要的一切——食堂、宿舍和图书馆。还有最为重要的安静氛围。北邮的沙河校区我就非常的喜欢,现代化的校区位于僻静的郊外,简直就是学习的圣地。但是像本部之类的地方还是尽快从地球上消失好一点——全校共用的公共澡堂、永远没有位置的图书馆简直就是上世纪八十年代的大学校园,在这里学习是一种挑战。至于在家里学习,我的评价是吾其还也。
> 关于这八个月我有一个更加详细的总结,我在这里就不赘述了。
### 搬校区&大二
进入8月份国内的疫情终于趋稳开始准备回到阔别已久的校园。
可是关于返校的第一个消息就是我们要从美好的沙河校区搬到傻逼的本部——这不仅仅是一个空间上的移动更是一种时间上的移动——沙河校区兴建与2015年而本部则是上个世纪的遗产。
> 这里插播本部笑话二则。
>
> - 欢迎来到北京邮电大学西土城校区,这里有上世纪八十年代北京最好的大学宿舍。——北京邮电大学招生广告
>
> - 年近耄耋的老校友走进学生二公寓,拉了一下墙上的拉绳开关,灯亮了,老校友激动的感叹到”还和我当年住的时候一模一样“。——校庆日见闻
很高情商的说,在本部生活有一种历史的厚重感。特别是你在教室上课的时候注意到墙上展示五十年前校风光的照片上展示的教学楼和你现在正在待的教学楼一模一样时,特别是你晚上提着桶穿过半个校园去洗澡时,特别是你参观老式办公楼一般的图书馆时。
虽然本部确实挺垃圾的,但是吐槽还是得有个限度。
大二一开始是就是大一学年的综合评价。我对此的评价是希望信息黄埔还是做点信息黄埔该做的的事情吧,不要搞得大家天天骂你信息黄泉,明明可以直接让系统自动生成的东西还需要自己手动填写,简直就是垃圾。
在综合评价完成之后,奖学金的评选和专业分流就是顺理成章的事情了。小小的从学校赚了一笔钱,成功进入了自己心仪的计算机科学与技术专业,从这个方面上来看,本学期还算是顺利。
然后是学生组织。执行自己在上半年使所指定的规划,从院团委的宣传部门跳槽出来,加入了传说中北邮最神秘的学生组织——北邮人。~~虽然就目前来看,北邮人略显的有点那啥~~。
然后是大创。作为业界知名的PPT大赛而且还能够在综合评价的时候加分这种你好我好大家好的东西自然是应参加尽参加~~我对于自己大一忘记水一次的这种事情表示:当事人很后悔~~。
小小的总结一下2022年可以算得上是一事无成的一年还搞砸了不少的事情。在写代码上进展有限成绩上大幅倒退说好的六级英语和大学物理竞赛都没有参加在年末应对疫情进展的时候更是把“不知所措”这个成语诠释的淋漓尽致。
![](./2022-final/2022-12-30-14-26-19-QQ_Image_1672381538441.webp)
关于今年的人际交往和社会关系我愿意用QQ2022年年终总结中的一张截屏来总结这张图片透漏出一种无可救药的悲伤。
![](./2022-final/2022-12-30-14-28-12-QQ_Image_1672381543836.webp)
## 展望
2023年很有可能还是极为困难的一年不过有了2022年的前车之鉴我相信我们可以在2023年取得比较辉煌的成就。
在思想上,我们提高自己认识困难,处理困难的思想和意识。要摒弃被动迎战的“怕事”思维,积极主动地按照形势的变化做准备。
在疫情的防控方面,我们需要尽快的适应新的疫情流行形势下的工作学习和生活,坚持以科学为导向,坚持生活至上的原则,在保证自己的生命安全的前提之下,尽量的维持原有的生活和学习习惯。在被动适应的同时我们还要主动出击,将工作做在前面,提高自己应对这种黑天鹅事件的能力。积极主动的关心新变异株的流行趋势,预计下一次疫情高峰,采取有力措施避免同病毒正面硬刚。提高自己的防护意识和防护水平,尽量避免一切可能的高危暴露。
在个人的学习方面,坚持学习是为了能力而不是为了分数的思想指引。关注个人实际能力和水平的提升,不要将时间浪费在无意义的竞争之上。采取信息化的措施优化自己的时间管理、目标管理,坚持利用信息化的方法管理自己学到的知识,坚持利用信息化的途径进行学习。要更多的对未来进行前瞻性的思考,通过文字的方式记录自己的思考成果,在这里方面要“务虚”而不是“务实”,更多的对前进的方向进行大方向上的把握。
我们相信2023年将是收获的一年。

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,88 @@
---
title: 2022年暑假碎碎念
date: 2022-08-22T15:39:13.0000000
tags:
- 杂谈
---
在8个月的漫长寒假的最后两个月~~也就是俗称的暑假中~~,我都干了些什么?
<!--more-->
### 学习
- 七天速通`C++``Qt`,搞定了上个学期的计算导论大作业,再用七天的时间整理代码和~~瞎编~~撰写文档,希望能得到一个不错的分数,挽救我那岌岌可危的平均分。
- 开始阅读`CSAPP`~~也就是传说中的没人理解计算机系统~~但说实话阅读的进度真的聊胜于无我用了整整一个月才读完第二章做完对应的Lab甚至Lab中的少数题连网上的题解都不咋看得懂开始怀疑自己是不是真的适合学习这个学科。目前希望在假期的最后四天里面能够创造奇迹拆弹成功~~指完成第三章的`Bomb Lab`~~。
> 学习日记:
>
> 7月1日刷B站
>
> 7月2日刷B站
>
> 7月3日刷B站刷到了其他程序员分享自己学习经历的视频深感焦虑决定明天开始好好学习重新做人
>
> 7月4日刷B站
>
> > 此处致敬了胡适之先生的日记,特此说明
- 开始学习伯克利的`cs61b`,虽然在假期的后半段才开始学习。
- 更新了三篇博客。分别记录了我在学习`Qt`中的一点小收获,我在小组合作中的一点思考和我对未来主力语言选择的迷茫。
- 下定决定要参加下一学期的物理竞赛,但是在听了讲座之后直接决定开学再开始学习,~~我知道我在家没法学习,俗称开摆~~
- 又捡起了`Blender`,并在[Github](https://github.com/tanjian1998/bupt_minecraft)上找到了伟大的前辈们在`Minecraft`里复刻的老校区,希望能用`Blender`渲染几张图当作桌面。
![唯一的一张成品](result1.webp)
> 在此感谢所有为此付出过汗水的前辈们,让我这个即将搬入老校区的萌新能提前一睹老校区的风采。
### 代码
- 继续完善我的[邮历](https://github.com/jackfiled/post_calendar_android),虽然目前开发由于一些技术之外的原因暂停了。
- 花了半个月的时间学习`Go`的API开发给[通知大全](https://squidward.top/)的后端做了一点[小更改](https://gitee.com/zoctopus/ddlgo-back/pulls/2)
> `Go`设计的挺好的,就是有点差。
- 写了一个从教务系统自动获得课表和生成`ICS`日历文件的[微服务](https://github.com/jackfiled/JWGLService),合并到上面提到的后端中。
- 做了`LeetCode`上的第一题。
### 游戏
- 买了文明六,但是一分钟都没玩
- 每日原神清委托,抽到了宵宫。
- 入坑崩崩崩,但是已经领了两次回归奖励了。终于推到了第十二章。
> 在第九章一睹传说中“最后一课”的风采。
> 也许我可以把我的Steam账号租出去了买了二十几个游戏总游戏时长还不超过200个小时目前一共就`战地五` 和`CS:GO`这两个游戏我玩上了50个小时。
### 番剧
这个假期看完了:
- 路人女主的养成方法
- 徒然喜欢你
- 中二病也要谈恋爱
- 女武神的餐桌
> 邪王真眼是最强的
现在正在看:
- 终将成为你
> 首先我不是白河豚,其次我不是白河豚,最后我不是白河豚。
### 目前手上还没填上的坑
- 修改我博客的主题。由于现在我用的主题在2017年之后就不在更新bug啥的已经不少了也缺少了一些我想要的功能打算在原本的基础上小修小补。
- 把我的树莓派改造好。在我的期望中这个玩意儿应该是一台全自动的BT做种机寝室里的影音中心再加上我的Arduino单片机应该还可以自动检测寝室的温湿度和控制空调的启动。
- 假期入了[稚晖君](https://space.bilibili.com/20259914)的[Holo Cubic](https://www.bilibili.com/video/BV1VA411p7MD),花了我大几百块钱,但是买回来之后就一直处在吃灰的状态,希望这个学期能有时间来折腾一下这个玩意儿。
### 其他
- 决定退出融媒体中心的综合影视部。
- 和前女友和平分手,希望她能在南京好好的生活。
- 终于参加了一次原魔群线上~~高峰论坛~~面基,见了见久违的高中同学们。

BIN
source/posts/2022-summer-vacation/result1.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,54 @@
---
title: 2023年年终总结
date: 2024-02-29T20:18:19.0000000
tags:
- 杂谈
- 年终总结
---
虽然2023年已经过去了两个月但是年终总结还是要发的。
<!--more-->
### 奋进的一年
虽然2022年在混乱中落幕没有为2023年的开幕“奠定一个良好的基础”但在呈上启下之际我们采取果断措施及时适应当前的新形势和新环境成功调整好自己的各种状态。在2023年的工作和学习中取得良好的成绩。
在年初整个人处在消极和悲哀的环境中。从外部大环境上看疫情防控开始转段正常的工作和学习节奏被打乱——或者说整个2022年最缺少的东西就是确定性从个人小环境上看2022年年中同高中的女朋友分手秋季学期存在着离散数学下、数据结构两门我没有学懂的课程学习和生活环境也从条件较好的新校区变成条件较差的本部校区还附带着寝室重新组合等等一系列问题。令人感到遗憾的是面对艰难繁重的工作任务我并没有及时调整好心态反而陷入“面临失败——状态下滑——更加失败”下降螺旋中。上述问题造成最直接的结果就是在大二的上学期取得了大学以来最差的成绩。
2023年就是在这种环境中开始幸而从2023年开始调整自己的状态一是在变化多端的环境中寻找一个长期性的目标紧紧围绕这个目标进行工作给予自己确定性二是主动切断各类社交媒体和零散信息源避免自己接触各种无意义的焦虑和内耗消息三是培养自己阅读的习惯。
从春季学期的成绩来说还是取得了一定的成绩:从需要考试的课程上来看,除了计算机组成原理这门课程的成绩不算理想,计算机网络和形式语言与自动机两门课程还是取得个人较为满意的成绩,而且最重要的是三门课程都还是学的比较透彻。从含有大作业的实践课程上看,数据结构的课程设计让我第一次完整实现了一个前后端分离的应用程序,计算机网络的课程设计锻炼了基于`C`的网络编程,而面向对象的程序设计和形式语言与自动机的实验则在`Java FX``Electron`等其他技术领域进行了探索。而在技术之外,通过多次大作业的合作锻炼了一个合作良好的团队——[post-guard](https://github.com/post-guard)。
相对于寒假,暑假则是一个非常充实的。在暑假开始之初前往南京“处理历史遗留问题”,在回到家之后又找了一个事业单位的信息科见习,在各种意义上提高了自己对于世界的认识。同时通过大创项目进入实验室中学习,开始第一次“体验”科研。
在秋季学期中,面对着操作系统、编译原理、数据原理和算法分析四门重量级的课程和为数不少的选修课,我们延续上学期调整好的状态积极进行面对,取得了良好的成绩。在开学之初参加数学建模比赛获得一个安慰奖,为数学竞赛和物理竞赛贡献了不少的报名费,在这些学科竞赛中虽然没有取得很好的成绩,但是参加这些赛事的经历本身也是一种非常难得的锻炼。
在2024年中我们利用好在2023年奠定的良好基础赓续奋发图强的精神动力更加从容自信地面对前进道路上的各种困难。
### 技术上的进展
虽然2023年是在学习上非常忙的一年但是在技术上还是取得不少的进展。
第一是基本确定了自己的技术路线:以.NET为中心技术在底层开发上练习使用Rust语言在前端界面上练习使用React。如此三个方向的进展基本覆盖了计算机开发技术的所有方面。.NET作为一门较为冷门的面对对象开发语言有着广泛的应用场景在国内尤其是在工控上位机领域应用广泛。Rust作为C++语言的上位替代有着极高的性能和开发难度因此我目前主要作为刷算法题的语言使用。React是目前个人认为最主流的前端开发框架我也很喜欢其函数式的界面设计思想。
第二是将自己的工作环境基本迁移到Linux系统中。Linux作为现代程序运行的主流环境有着很多的好处和坏处好处是对于各种开发工具的兼容性都非常好坏处是图形用户界面是一坨屎。因此我的系列博客“日用Linux挑战”还在火热连载中
第三是我和同学合资购买了一台二手服务器。在这台服务器上我们部署了各种各样的服务和应用,在很大程度上重塑了我的开发工作流乃至于不少生活习惯。鉴于这一点有点离题,我会单开一篇文章进行介绍(开始挖坑)。
展望2024年的技术前进方向我们希望向语言和系统更深的地方进军。深入研究.NET运行时的方方面面兼顾熟悉在.NET生态系统中的各种常用框架技术。尝试推动将Rust语言从算法界面普及到实际应用开发场景中去以嵌入式开发和操作系统开发等为抓手逐渐取代C++语言之前在开发技术工具箱中的地位。对于React在目前所有东西都运行在浏览器上的大环境下完全不懂任何前端显然是不合理的因此合理适度的学习相关知识是必不可少的。
之于Linux我希望继续坚持使用Linux。在日常使用的过程中尝试将上述各种技术同Linux的系统开发和应用开发结合起来为不断完善Linux做出一点贡献。
### 其他有趣的方面
2023年最令我吃惊的事情是我刷B站的时长
![image-20240303165826486](2023-final/image-20240303165826486.webp)
容易计算得出我一共看了64天的B站接近六分之一的时间都在看。虽然我确实有着在干活的时候黑听B站和把B站当作音乐播放器的习惯但是这个时间未免有点太长了。下一年一定要在这个方面做出一定的改变将更多的时间放在看书上面去~~虽然写这句话的时候我就在黑听B站~~。
说到看书,在这一年内看来不少的书。在这学期看的最多的还是各种日式轻小说。总体上来说作为一个死宅,我对于各种校园恋爱题材的轻小说还是比较感兴趣的,本年度看完的就有:《我的青春恋爱物语果然有问题》,《路人女主的养成方法》,《继母的拖油瓶是我的前女友》,《关于邻家的天使大人不知不觉把我变成废人这件事》。不过对于各种涉及到世界观的日轻作品我就比较拒绝了,感觉不带脑子看还行,带脑子就有点不行,不过应该还是有不少优秀的作品等待我发现。在中文小说上,这年度应该就看了两本:《北平无战事》和《临高启明》,虽然这两本书在各种意义上差距都有点大。在各种专著上我的兴趣点也不少,比较好看的有《美国反对美国》和《筚路蓝缕 世纪工程决策建设记述》比较枯燥的有《当代中国政府与政治》和《走向自主创新寻求中国力量的源泉》和《新火走向自主创新2》。不过后面两本工业调查报告如果看进去了还是比较上头的那种工业上“冷峻的热血”非常激动人心。
还有不少优秀的电视剧作品。例如《大决战》三部曲和《决战之后》这几部描写解放战争的老电视剧,《三体》这个新锐电视剧虽然在节奏上有点拖沓,但是各种大场面的观感不错和合理的改变还是不错的。

BIN
source/posts/2023-final/image-20240303165826486.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,90 @@
---
title: 2024年年终总结
date: 2025-01-16T17:15:05.8634370+08:00
tags:
- 杂谈
- 年终总结
---
欸,年终总结难道不是应该在新年当天发出吗,什么已经是新年第三天了?!
然而年末偶遇流感病毒,头疼脑热强如怪物,拼尽全力也无法战胜。
所以年终总结再次跳票,红豆泥私密马赛!
<!--more-->
### 压力
本年度的第一个关键词,我会选择压力。这一年总是被不同的压力笼罩着,先是有形的压力,然后是无形的压力,在不同的时间阶段有着不同的来源。
1月份起始的两周就是大三学年秋季学期的期末考试周而鄙人在下不才我在本学期面临着计算机科学四幻神的考验——老师不知所云之操作系统、抽象概念无法理解之编译原理、全英语授课之数据库系统原理和智商不够无法战胜之算法导论。挣扎在保研线上的我刚刚被上一学期的离散数学的~~75分~~74分和数据结构的79分拷打面对着如此沉重的考试压力加起来一共12学分呢可耻的失眠了。
过完年回来的三月份就是同论文奋斗的一个月。虽然只是一篇6页的EI检索论文但对于一个**纯洁**的本科生来说还是有点太困难了。这个过程就像是你先拉了一坨大的,然后在上面细细的涂上巧克力,在最后发表的过程中,需要在众人的面前大嚼这一坨东西,并且称赞“真是一道美食啊”!还没有开始的学术生涯就已经留下永恒的污点力(悲)。
搞完论文的四月和五月则是和大作业搏斗的两个月。首先是无法战胜的“编译原理课程设计”内容是设计一个Pascal-S到C语言的源到源编译器。这一大作业的主要压力来源是大作业本身的难度直到最后提交的时候全部95个测试点也没有能够完全通过然而其他人在祖传代码上缝缝补补却过来哭。虽然考虑到我们是全手写的编译器没有使用任何的编译器构建工具提出的解决方案也称不上是墨守成规老师给了我一个还算是可以的分数算是压力中的小小慰藉。
然后是风波不断的软件工程大作业明明只是一个相对简单的Web前后端开发但是我们前后进行了三次验收才通过一直拖到了学期的第16周。老师设计的联合验收制度给我们结结实实的上了一课要求联合验收小组的不同前后端需要能够任意组合使用导致我们为了适配另外一组的逻辑几乎是把核心代码写了两遍。虽然我不喜欢在背后攻击别人但是我不得不说这一年中最有压力的时刻往往不是自己的事情搞不定时而是看着别人搞砸事情你却无能为力的时候。
这两个月还夹杂这一个意义不明的专业实习,明明是计算机科学与技术专业的牛马,为什么会被中兴通讯的老师培训通信项目的项目管理?
应付完上面这些杂七杂八的内容,便是本科生生涯中的最后三场考试:人称计算机领域的政治之《软件工程》,通信领域科普课程之《现代交换原理》和永远的神之《计算机系统结构》。
不得不说《软件工程》,~~或者人们常说的肖概~~确实不愧于计算机领域的政治之称。毕竟政治的主要课题就是研究如何组织和动员人群以完成一个特定的目标,《软件工程》不过是将人员限制为了软件的开发人员,领域限制为了软件开发领域,基本的道理还是相通的。
《现代交换原理》则是一门在现有的课程体系下非常尴尬的一门课程,显然这门课的保留还是为了凸显“计算机+通信”的学科特色,但是大量前置知识的缺失和同其他课程的脱节使得这门课就显得非常的“脱节”。而且相对来说,通信技术的发展速度远远不如互联网的迭代技术,这门课也被同学们戏称为“古代交换原理”。令人最难受的,虽然知识古代,但是却一点都不简单,很多内容只能说是听了个概念,幸好最后的考试不难,靠死记硬背通过了考试。
《计算机系统结构》就是核心课中的核心课了。课程内容和《计算机组成原理》衔接的非常紧密,~~虽然我组成原理就学的很垃圾~~主要围绕着如何最大限度的并行化运行程序进行从指令级的并行一直到多机并行可以说是压力最大的一门考试。在准备的过程中做了很多套往年题博客上也发布了一部分的复习笔记最终幸好低空飞过。唯一的吐槽是实验什么时候可以从MIPS改成为RISC-V呢。
三门课的考试一结束,这些死线明确的、有形的压力便消失了,但是无形的压力——对于是否能保研的焦虑——便笼罩下来。
7月和8月都是在这种不安和恐慌中度过这种氛围在9月份保研名单出炉之前达到了顶峰。保研的流程开始之后则是通知推着人走各种交材料各种准备答辩各种等待公示直到最后的保研名单出炉。
不过现在回想起来,最后名单出炉,获得保研资格,复试通过之后,并没有一种如释重负的感觉,或者说终于实现了既定目标的快感。反而是一种“啊,结束了”的空落感,只想回去睡一觉。
然后新的~~风暴~~压力已经出现,在度过一个短短的国庆假期之后便正式进组,作为一个研究生的社畜生涯就此开始。
### 经历
虽然2024年的第一个关键词已经选择为“压力”但是众所周知高压锅里往往能压出好吃的。人也是这样。所以我将2024年的第二个关键词定为“经历”人生如逆旅我亦是行人各式各样的经历便是风格迥异的景点。
人生第一篇学术论文的撰写和发表无疑是今年最难忘的经历。虽然我在前面称之为“学术生涯上的污点”,但是污点也好过一片空白不是,还非常的引人夺目。而且这是一个完整的撰写-发表流程,从开始的选题、实验、撰写、投稿,到最后的接受、提交、发表、报销等等数个环节我均参加。这个过程不仅让我对于学术论文的诞生流程有力较为清晰的认识,也对学校的各种发表和报销流程有了深入的了解。
两个大作业编译原理课程设计和软件工程大作业也是非常难忘的经历。这两个项目的代码都已经整理好开源在Github上了。前者代表了目前我软件开发的最高水平而后者则是我本科阶段唯一一个差点失败的软件开发项目。这种冰火两重天的对比实在是很难令人忘记。
这两个项目中的收获有非常技术性的。相较于2023年面对各种大作业时的略显底气不足这次我在各种技术栈的选择上更加游刃有余选择了完全倒向.NET和React摈弃了之前的Java和Vue。各类现代软件开发技术也得到了充分的应用例如由Gitea Actions驱动的DevOps实践完全基于合并请求的多人协作流程。事实证明这些协作流程确实在一定程度上加速了项目的开发。
但是,“软件工程里没有银弹”,先进技术的堆叠并不能保证软件项目成功。虽然我这里~~自吹自擂~~有非常多新技术的帮助,软件工程大作业的差点失败的确说明了软件工程实际上还是人的工程,猪队友永远比凶恶的敌人更可怕。当然也不能将所有的锅都扔给别人,我在项目失控的过程中也没有能够采取有力的措施挽救整个项目,~~负有不可推卸的领导责任~~。
今年最后一个难忘的经历便是去横店镇参加CNCC 2024也单独出过[博客](https://rrricardo.top/blog/essays/cncc-2024)。虽然之前学术论文发表的过程中也是在学术会议上做过口头报告,不过是线上参加的,并没有特别的实感。现在线下参加,也不需要自己上去发表,顿感旅游真好玩,~~也有可能是因为CNCC比较水~~。
### 匆匆
2024年的第三个关键词我想定为”匆匆“虽然想找一个更加”有文化“的词汇奈何自己的文化造纸实在不够故定为”匆匆“。
可2024年确实是非常忙碌的一年现在回想起来几乎每一个月都是在为了某一件特定的事情而奔走着。还记得在新年伊始的时间里我还制订了各种各样的读书计划和补番计划现在看来定计划的目的不是为了实现而是为了安心。
不过匆匆之中还是读了几本书。首先是久负盛名的《置身事内——中国政府与经济发展》,这本书的开篇即言:“这本书是写给大学生和对经济话题感兴趣的读者”,细读下来也确实如此。然后是一本我从小便着迷的二战军史相关话题《美国陷阱:橙色计划始末》,其中若干的政治与军事细节之于我不过是走马观花,不过其中表达出的长期战略实在令人敬佩。
至于补番计划我则是表现出了同电子ED一样的症状对于新番没有兴趣对于补早就下载安装好的老番更是兴趣缺缺。反倒是电视剧由于12月韩国的惊天一变我又重新下载了《第五共和国》
不过我的B站观看时长再度增长30%,这好吗,这不好,~~有这么多时间刷B站鬼知道你匆匆在哪了~~。
![image-20250115171809775](./2024-final/image-20250115171809775.webp)
### 未来
> 定计划的目的不是为了实现,而是为了安心。
站在年关已经可以预见到2025年将会是更为繁忙的一年从一月份到十月份都已经有了或多或少的安排现在无法多言只能希望都能有良好的结果。
还是多说点可以说的罢。
首先是读书计划。《置身事内——中国政府与经济发展》的每章最后都有一个推荐书目一整本上总结下来也能有超过50本其中不乏超过一千页的大部头说能够一年看完显然是痴人说梦。这里先列两本同我的工作关系密切的书籍
- 陆风,《光变:一个企业及其工业史》
- 吴军,《浪潮之巅》
其次是补番计划,这一年刷到了不少押井守导演的《机动警察》系列,虽然我之前对于人形机器人并不热心,但剧中精细的作画和宏大的背景设定确实非常吸引人,遂决定今年找来看看。

BIN
source/posts/2024-final/image-20250115171809775.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,228 @@
---
title: 日用Linux挑战 第5篇 标准安装流程
date: 2024-7-16 20:08:37
tags:
- Linux
- 技术笔记
---
标准化ArchLinux的安装流程是日用Linux道路上一个重要的里程碑。
<!--more-->
## 概述
本安装流程的目的在于规范化一台设备安装ArchLinux的过程通过明确各个步骤选择的软件和格式确保不同的设备之间有着良好的互操作性减少维护各种不同安装配置的ArchLinux示例的心智负担。
具体而言,本标准操作流程主要试图标准化如下几个问题:
- 进行硬盘分区时使用什么格式?分成几个区?
- 使用`pacstrap`安装时,应该安装哪些软件包?
- 在编辑`locale.gen`时,应该选择哪些`locale`
- 主机名是否应该设计统一的规则进行设置?
- `boot loader`的选择和配置?
- 图形化环境应该如何选择?
同时本标准操作流程亦是一份ArchLinux系统安装指南但是流程中将更多的注重于应该做什么而不是解释为什么要这样做。
## 在安装开始之前
在启动`Live CD`环境之后,首先进行如下的操作。
验证启动模式是否为64位
```shell
cat /sys/firmware/efi/fw_platform_size
```
连接互联网:
- 如果是有线网应该可以自动进行连接;
- 如果是无线网,使用`iwctl`进行连接。
测试到互联网的连接通畅之后,同步系统时钟:
```shell
timedatectl
```
### 硬盘格式化
首先使用`fdisk`工具对需要安装系统的磁盘进行分区,系统一般情况下使用`UEFI`进行启动,磁盘使用`GPT`分区表。各分区的参数如下表所示。
| 挂载的位置 | 大小 | 分区的类型 | 分区后的设备号(示例) |
| ---------- | -------------- | ---------------- | ---------------------- |
| /boot | 1G | EFI System | /dev/nvme0n1p1 |
| / | 磁盘余下的大小 | Linux root (x86) | /dev/nvme0n1p2 |
对分区好的磁盘进行格式化。
```shell
mkfs.fat -F 32 /dev/nvme0n1p1
mkfs.btrfs -L ArchLinux /dev/nvme0n1p2
```
`/dev/nvme0n1p2`挂载到`/mnt`目录中,对`btrfs`文件系统进行顶级`subvolume`的划分,具体划分如下表所示:
| subvolme名称 | 挂载的位置 | 是否打开写时复制 |
| ------------ | ----------- | ---------------- |
| @root | / | 是 |
| @home | /home | 是 |
| @swap | /swap | 是 |
| @var | /var | 否 |
| @snapshots | /.snapshots | 是 |
完成顶级`subvolume`的划分之后,取消`/mnt`的挂载,使用`subvol`选项进行挂载:
```shell
mount --mkdir /dev/nvme0n1p2 /mnt -o subvol=@root
mount --mkdir /dev/nvme0n1p2 /mnt/home -o subvol=@home
mount --mkdir /dev/nvme0n1p2 /mnt/swap -o subvol=@swap
mount --mkdir /dev/nvme0n1p2 /mnt/var -o subvol=@var
mount --mkdir /dev/nvme0n1p2 /mnt/.snapshots -o subvol=@snapshots
```
设置一个和内存大小相同的`swap`文件下面的指令假设机器的内存大小为16G
```shell
btrfs filesystem mkswapfile --size 16g --uuid clear /swap/swapfile
swapon /swap/swapfile
```
挂载`EFI`分区:
```shell
mount --mkdir /dev/nvme0n1p1 /mnt/boot
```
## 安装系统
首先是选择合适的镜像源,这里推荐的几个镜像源为:
```
Server = https://mirrors.bupt.edu.cn/archlinux/$repo/os/$arch
Server = https://mirrors.bfsu.edu.cn/archlinux/$repo/os/$arch
Server = https://mirrors.tuna.tsinghua.edu.cn/archlinux/$repo/os/$arch
Server = https://mirrors.cernet.edu.cn/archlinux/$repo/os/$arch
```
同时调整一些`pacman.conf`中的设置打开输出颜色将并行下载设置为8。
使用`pacstrap`安装需要的软件包,具体的软件包列表如下:
| 软件包名称 | 用途 |
| -------------- | ------------------- |
| base | 基础软件包 |
| base-devel | 基础开发软件包 |
| linux | 系统内核 |
| linux-firmware | 系统固件 |
| btrfs-progs | `btrfs`文件系统工具 |
| networkmanager | 网络连接工具 |
| vim | 文本编辑器 |
安装的指令如下:
```shell
pacstrap -K /mnt base base-devel linux linux-fireware btrfs-progs networmanager vim
```
## 配置系统
首先生成`fstab`
```shell
genfstab -U /mnt >> /mnt/etc/fstab
```
注意生成之后验证生成文件。
使用`chroot`进入安装的新系统:
```shell
arch-chroot /mnt
```
配置系统时间和硬件时间:
```shell
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
hwclock --systohc
```
配置系统的本地化,编辑`/etc/locale.gen`,取消下面这些区域设置:
| 名称 | 解释 |
| ----------------- | -------------- |
| en_US.UTF-8 UTF-8 | 英语,美国 |
| en_GB.UTF-8 UTF-8 | 英语,大不列颠 |
| zh-CN.UTF-8 UTF-8 | 中文,中国 |
编辑好之后,使用`locale-gen`生成本地化选项。配置系统的默认本地化选项,创建`/etc/locale.conf`
```shell
LANG=en_GB.UTF-8
```
编辑`/etc/hostname`,在文件中填入系统的主机名,系统中的主机名一般为当前主机的型号。
使用`passwd`设置`root`用户的密码。
安装`grub`
```shell
pacman -S grub efibootmgr
grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB
grub-mkconfig -o /boot/grub/grub.cfg
```
退出`chroot`的系统,取消挂载硬盘,重启系统退出`Live CD`环境:
```shell
umount -R /mnt
reboot
```
## 安装图形化界面
新安装的系统启动之后,进行普通用户的创建和图形化界面的安装。
首先创建普通用户并添加到`sudo`用户组中。
```shell
useradd -m ricardo
pacman -S sudo
usermod -aG wheel ricardo
```
使用新创建的用户登录系统,在新用户的目录下创建一些不需要进行快照的`subvolume`
```shell
btrfs subvolume create .cache
btrfs subvolume create .wine
chattr +C .cache
chattr +C .wine
```
安装`plasma`图形化界面。
```shell
sudo pacman -S plasma sddm
sudo systemctl enable sddm.service
```
## 硬件相关联的操作
### CPU
按照CPU的厂商分别安装`intel-ucode`或者`amd-ucode`两个微码文件。
### GPU
按照GPU的厂商分别安装对应的驱动程序。
同样的,在使用`NVIDIA`显卡和`Wayland`显示协议,仍然需要配置对应的驱动参数:
```
options nvidia_drm modeset=1 fbdev=1
```

View File

@@ -0,0 +1,407 @@
---
title: 在ASP.NET Core中集成认证和授权流程
date: 2024-09-08T22:27:17.0328669+08:00
tags:
- ASP.NET Core
- 技术笔记
---
以[Martina](https://github.com/post-guard/Martina)为例记录如何典型的ASP.NET Core应用中集成认证和授权的流程。
<!--more-->
## 业务需求概述
[Martina](https://github.com/post-guard/Martina)系统是一个酒店的空调和入住管理系统,项目中对于认证和授权的要求是一个典型的多权限、多用户模式,具体来说:
- 系统中所有的接口均需要在登录之后才能调用;
- 系统中安装不同管理领域将用户的权限划分为一大类、三小类:一个超级管理员权限和客房、空调、账单三个领域管理员权限;
- 普通用户的权限有时间和使用房间的要求:只能在入住时间段内访问入住房间的空调相关接口。
可以看出上述这些要求基本上覆盖了一个常见系统的中所有关于认证和授权的使用场景因此本篇便以该系统为例介绍如何在ASP.NET Core框架中实现上述业务要求。
## 身份认证和授权的基础知识
身份认证是指由用户提供凭据,然后将其与存储在操作系统、数据库、应用和资源中的凭据进行比较的过程。而授权过程发生在身份认证成功之后:在凭据匹配成功之后,用户身份验证成功,可执行已向其授权的操作。授权就是判断允许用户执行操作的过程。
在ASPNET.Core中这是通过两个**中间件**`UseAuthenication``UseAuthorization`来完成的,还是来看这张经典的中间件工作流程:
![ASP.NET Core 中间件管道](./aspnet-authorization/middleware-pipeline.svg)
可以看到在中间件的管道中认证中间价将在授权中间件运行之前运行——这两个顺序是不能颠倒的如果授权中间件在认证中间件运行之前运行那授权中间件就无法为用户授予任何权限所有需要权限的接口均会返回401错误码。
> 为什么我知道的如此清楚捏?
>
> 因为我真的写反过最后还是在框架代码里面打断点才发现授权中间件拿不到用户登录的信息当时还在GitHub的工单里面翻找相关的bug感觉可以评选为人生十大傻逼bug之一。
概览完认证和授权之后,首先来谈谈认证。认证的基本过程就是一个开锁的过程:用户提供一个凭据,也就是钥匙,系统验证凭据的有效性,就是锁的工作。这里主要的问题就是这个钥匙的形状长什么样子,也就是凭据的表现形式。常见的凭据表现形式有`Cookies``JWT`两种。
`Cookies`是一种服务器发送到用户浏览器并保存在本地上的一小块文本文件,用户浏览器在保存这些文本文件之后会在每次向同一服务器发送请求时在请求体中携带一些文本文件信息。`Cookies`是一种非常古老的技术这种技术使得无状态的HTTP协议可以记录稳定的状态信息因此在这个技术常被应用来认证网络用户的身份。
`JWT`的全称是JSON Web Token是一种使用JSON对象表示格式在两方之前安全且有效的传输信息的方法使用该方法的信息可以使用指定的密钥或者是公钥-私钥对验证信息的有效性。因此`JWT`作为一种通用的、可验证的令牌格式用来完成网络中认证的过程。在服务器验证某一个用户的身份之后(例如通过验证账号密码、通过第三方的验证)可以签发一个`JWT`令牌给用户浏览器,浏览器可以使用`localstorage`等技术将该令牌存储在用户浏览器中并在每次向服务器发送请求的过程中将该令牌携带在一个特定的请求头`Authorization`中。
> 在`Authorization`请求头中常常会以`Bearer <JWT>`的格式进行,这其中的`Bearer`是指定的身份认证的模式Scheme这里的详细解释可以见[MDN文档](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication)。
谈完认证之后,再来看看授权。授权的实现是一个和业务逻辑高度相关的过程,一个常见的业务逻辑是用户分为不同的层级——例如普通用户和管理员,而不同层级的用户可以调用的接口不同,这就是**基于策略的授权模式**的典型应用场景,该模式允许为每个接口指定一个或者多个认证策略。另外一个常见的业务逻辑是用户只能访问自己所拥有的资源——例如用户只能删除自己创建的记录,这就是**基于资源的授权模式**的典型应用场景,该模式允许为一种资源编写一段授权逻辑,并通过依赖注入的方式供服务器或者控制器使用。
## 身份认证和授权的实践
在本个系统中,身份认证将采用`JWT`令牌而授权的部分将会覆盖到上文中提到的两种典型模式通过研究本系统的实现可以理解在ASP.NET Core中集成身份认证和授权的流程。
在ASP.NET Core系统中集成`JWT`令牌的认证方式需要先安装一个包`Microsoft.AspNetCore.Authentication.JwtBearer`
### 身份认证部分
身份认证部分主要分为令牌签发和令牌验证两个部分,令牌认证的部分主要在于使用`AddAuthentication`向主机容器中注入服务,而令牌签发的部分则通常是实现一个接口,在验证用户输入的账号和密码之后生成该用户对于的令牌。这两个过程是高度关联的,在签发过程中设置的令牌信息需要在验证令牌的过程设置对应的部分,否则签发的令牌就无法验证。因此先介绍签发令牌的部分。
签发令牌之前先介绍一下`JWT`令牌的组成,一个兼容的`JWT`令牌一般有三个部分组成:
- 头部`Header`:头部在一般情况下只有两个字段组成,一个`tpy`字段存储固定值为`JWT`指定这是一个`JWT`令牌,一个`alg`字段指定验证该令牌的算法是`HMCA SHA256`还是`RSA`
```json
{
"alg": "HS256",
"typ": "JWT"
}
```
- 负载`Payload`:包含各种关于实体(用户)的宣称列表。宣称可以分成三种类型,已注册的类型、公开的类型和私有的类型,这三种的类型的区别可以从[RFC7519](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1)中具体查看,简而言之就是已注册的类型就是推荐在签发令牌时设置的,包括签发者和到期时间等的内容,公开的类型是公开注册可以共享的名称,而私有的就是自行指定的。
```json
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
```
- 签名`signature`:验证令牌的签名部分,在使用`HMCA SHA256`算法的情况下,签名的计算公示如下所示:
```
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
```
在学习了这些`JWT`的基础知识之后就可以很容易的写出如下的令牌生成代码:
```csharp
public string GenerateJsonWebToken(User user)
{
List<Claim> claims =
[
new Claim(ClaimTypes.Name, user.Username),
new Claim(ClaimTypes.NameIdentifier, user.UserId)
];
JwtSecurityToken token = new(
issuer: _option.Issuer,
audience: user.UserId,
notBefore: DateTime.Now,
expires: DateTime.Now.AddDays(7),
claims: claims,
signingCredentials: _signingCredentials
);
return _jwtSecurityTokenHandler.WriteToken(token);
}
```
签发令牌的凭据使用下面的方式创建:
```csharp
private readonly SigningCredentials _signingCredentials =
new(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jsonWebTokenOption.Value.JsonWebTokenKey)),
SecurityAlgorithms.HmacSha256);
```
签发的过程中部分重要的参数使用配置的方式提供,例如签发者和密钥,配置实体类如下所示:
```csharp
public class JsonWebTokenOption
{
public const string OptionName = "JWT";
/// <summary>
/// JWT令牌的签发者
/// </summary>
public required string Issuer { get; set; }
/// <summary>
/// JWT令牌的签发密钥
/// </summary>
public required string JsonWebTokenKey { get; set; }
}
```
签发好令牌之后就可以编写验证令牌的部分了:
```csharp
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(
options =>
{
JsonWebTokenOption? jsonWebTokenOption = builder.Configuration.GetSection(JsonWebTokenOption.OptionName)
.Get<JsonWebTokenOption>();
if (jsonWebTokenOption is null)
{
throw new InvalidOperationException("Failed to get JWT options");
}
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jsonWebTokenOption.Issuer,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jsonWebTokenOption.JsonWebTokenKey)),
ValidAlgorithms = [SecurityAlgorithms.HmacSha256]
};
});
```
在验证令牌的部分,指定验证令牌的签发者和签名。
编写完上述代码之后就可以增加身份验证和授权的中间件验证上述代码的正确性了。
```csharp
application.UseAuthentication();
application.UseAuthorization();
```
### 授权的部分
#### 按照策略进行授权
系统中一个典型的场景就是不同级别的用户能访问的接口不同,例如在本系统中用户的级别分为:
```csharp
[Flags]
public enum Roles
{
User = 0b_0000_0000,
RoomAdministrator = 0b_0000_0001,
AirConditionerAdministrator = 0b_0000_0010,
BillAdministrator = 0b_0000_0100,
Administrator = 0b_0000_1000
}
```
为了方便给不同的接口指定不同的访问策略首先创建一个对用户级别的要求Requirement
```csharp
public class HotelRoleRequirement(Roles hotelRole) : IAuthorizationRequirement
{
public Roles HotelRole { get; } = hotelRole;
}
```
然后实现一个处理该要求的验证程序:
```csharp
public class HotelRoleHandler(MartinaDbContext dbContext) : AuthorizationHandler<HotelRoleRequirement>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
HotelRoleRequirement requirement)
{
Claim? userId = context.User.FindFirst(c => c.Type == ClaimTypes.NameIdentifier);
if (userId is null)
{
return;
}
User? user = await dbContext.Users
.Include(u => u.Permission)
.Where(u => u.UserId == userId.Value)
.FirstOrDefaultAsync();
if (user is null)
{
return;
}
// 如果要求的权限是超级管理员
// 则判断是否是超级管理员
if ((requirement.HotelRole & Roles.Administrator) == Roles.Administrator)
{
if (user.Permission.IsAdministrator)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
// 剩下的权限
// 如果用户是超级管理员则直接有权限
if (user.Permission.IsAdministrator)
{
context.Succeed(requirement);
return;
}
if ((requirement.HotelRole & Roles.BillAdministrator) == Roles.BillAdministrator)
{
if (user.Permission.BillAdminstrator)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
if ((requirement.HotelRole & Roles.RoomAdministrator) == Roles.RoomAdministrator)
{
if (user.Permission.RoomAdministrator)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
if ((requirement.HotelRole & Roles.AirConditionerAdministrator) == Roles.AirConditionerAdministrator)
{
if (user.Permission.AirConditionorAdministrator)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
}
}
```
框架要求在处理程序使用依赖注入到主机的容器中,这里因为在验证的过程中使用了数据库的服务`DbContext`因此被注册为一个范围内Scope服务。
```csharp
builder.Services.AddScoped<IAuthorizationHandler, HotelRoleHandler>();
```
为了方便在`[Authorize]`注解中使用字符串指定不同的授权策略,在`AddAuthoriztion`进行配置:
```csharp
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Administrator", policy =>
{
policy.AddRequirements(new HotelRoleRequirement(Roles.Administrator));
});
options.AddPolicy("RoomAdministrator", policy =>
policy.AddRequirements(new HotelRoleRequirement(Roles.RoomAdministrator)));
options.AddPolicy("AirConditionerAdministrator", policy =>
policy.AddRequirements(new HotelRoleRequirement(Roles.AirConditionerAdministrator)));
options.AddPolicy("BillAdministrator", policy =>
policy.AddRequirements(new HotelRoleRequirement(Roles.BillAdministrator)));
});
```
使用该方法注册之后就可以直接在`[Authorize]`注解中指定需要使用的授权策略:
```csharp
[HttpGet("revenue")]
[Authorize(policy: "BillAdministrator")]
[ProducesResponseType<ExceptionMessage>(400)]
[ProducesResponseType<RevenueTrend>(200)]
public async Task<IActionResult> QueryRevenueTrend([FromQuery] DateTimeOffset begin, [FromQuery] DateTimeOffset end)
{
if (begin >= end)
{
return BadRequest(new ExceptionMessage("开始时间不能晚于结束时间"));
}
RevenueTrend trend = new()
{
TotalUsers = await managerService.QueryCurrentUser(),
TotalCheckin = await managerService.QueryCurrentCheckin(),
DailyRevenues = await managerService.QueryDailyRevenue(begin, end)
};
return Ok(trend);
}
```
#### 按照资源进行授权
系统中一个典型的需求就是一个用户只能修改资源池中部分自己拥有权限的资源,在本系统中就是用户只能开启和关闭当前入住房间中的空调。
按照资源进行授权的总体流程和安装策略进行授权总体上差别不大,除了无法在注解中设置需要使用的策略。首先仍然是设计一个授权的要求:
```csharp
public class CheckinRequirement : IAuthorizationRequirement;
```
然后为该要求实现一个授权处理程序,注意在这里集成泛型基类`AuthorizationHandler`时除了需要指定要求类还需要指定资源类型:
```csharp
public class CheckinHandler(
RoomService roomService,
MartinaDbContext dbContext)
: AuthorizationHandler<CheckinRequirement, Room>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
CheckinRequirement requirement,
Room resource)
{
Claim? userId = context.User.FindFirst(c => c.Type == ClaimTypes.NameIdentifier);
if (userId is null)
{
return;
}
User? user = await dbContext.Users.AsNoTracking()
.Where(u => u.UserId == userId.Value)
.FirstOrDefaultAsync();
if (user is { Permission.IsAdministrator: true } || user is { Permission.AirConditionorAdministrator: true })
{
context.Succeed(requirement);
return;
}
CheckinRecord? record = await roomService.QueryUserCurrentStatus(userId.Value);
if (record?.RoomId == resource.Id)
{
context.Succeed(requirement);
}
}
}
```
在使用该授权方法时,通过依赖注入获得一个`IAuthorizationService`的接口对象并调用对应的授权接口进行验证,传入需要访问的资源和当前`HttpContext`中的用户`User`,这个`User`实际上就是`JWT`令牌中的负载部分。
```csharp
AuthorizationResult result = await authorizationService.AuthorizeAsync(User, room, [new CheckinRequirement()]);
if (!result.Succeeded)
{
return Forbid();
}
if (!airConditionerManageService.VolidateAirConditionerRequest(roomObjectId, request, out string? message))
{
return BadRequest(new ExceptionMessage(message));
}
```
## 总结
通过清晰的定义身份认证和授权两个环节并提供了一个要求——处理程序的授权模型ASP.NET Core提供了一套简单易用、扩展性高的接口安全系统。

View File

@@ -0,0 +1,324 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 706.35 390">
<defs>
<symbol id="New_Symbol_125" data-name="New Symbol 125" viewBox="0 0 32 16">
<path d="M31,8c0,3.85-3.6,7-8,7H9c-4.4,0-8-3.15-8-7S4.6,1,9,1H23C27.4,1,31,4.15,31,8Z" fill="#fff"/>
<path d="M23,16H9c-5,0-9-3.59-9-8S4,0,9,0H23c5,0,9,3.59,9,8S28,16,23,16ZM9,2C5.14,2,2,4.69,2,8s3.14,6,7,6H23c3.86,0,7-2.69,7-6s-3.14-6-7-6Z" fill="#0072c6"/>
</symbol>
</defs>
<g id="Shapes">
<rect width="706.35" height="390" fill="#fff"/>
<g>
<rect x="376.35" y="287.58" width="215" height="85" fill="#fff"/>
<rect x="376.35" y="287.58" width="215" height="85" fill="#3c3c41" opacity="0.05"/>
<rect x="376.35" y="287.58" width="215" height="85" fill="none" stroke="#3c3c41" stroke-miterlimit="10" stroke-width="0.25"/>
</g>
<use width="32" height="16" transform="translate(73.58 22.58)" xlink:href="#New_Symbol_125"/>
<g>
<path d="M47.15,70.41c0,3.85-3.6,7-8,7h-14c-4.4,0-8-3.15-8-7s3.6-7,8-7h14C43.55,63.41,47.15,66.56,47.15,70.41Z" fill="#fff"/>
<path d="M39.15,78.41h-14c-5,0-9-3.59-9-8s4-8,9-8h14c5,0,9,3.59,9,8S44.11,78.41,39.15,78.41Zm-14-14c-3.86,0-7,2.69-7,6s3.14,6,7,6h14c3.86,0,7-2.69,7-6s-3.14-6-7-6Z" fill="#76bc2d"/>
</g>
<g>
<rect x="79.78" y="57.58" width="110" height="25" fill="#fff"/>
<rect x="79.78" y="57.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="89.78" y1="39.15" x2="89.78" y2="52.2" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="86.04 51.1 89.78 57.58 93.52 51.1 86.04 51.1" fill="#005ba1"/>
</g>
<g>
<rect x="104.78" y="97.58" width="110" height="25" fill="#fff"/>
<rect x="104.78" y="97.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="114.78" y1="82.89" x2="114.78" y2="91.94" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="111.04 90.85 114.78 97.33 118.52 90.85 111.04 90.85" fill="#005ba1"/>
</g>
<g>
<line x1="79.78" y1="70.41" x2="50.91" y2="70.41" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M57.06,66.09a.44.44,0,0,1-.13.62l-5.81,3.7,5.81,3.7a.44.44,0,0,1,.13.62.45.45,0,0,1-.62.14L50,70.79a.44.44,0,0,1-.21-.38A.47.47,0,0,1,50,70L56.44,66a.55.55,0,0,1,.24-.07A.45.45,0,0,1,57.06,66.09Z" fill="#4f4f4f"/>
</g>
<g>
<rect x="129.78" y="137.27" width="110" height="25" fill="#fff"/>
<rect x="129.78" y="137.27" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="139.78" y1="122.58" x2="139.78" y2="131.63" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="136.04 130.53 139.78 137.01 143.52 130.53 136.04 130.53" fill="#005ba1"/>
</g>
<g>
<path d="M129.78,148.93h-10a5,5,0,0,1-5-5V123.68" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M119.09,129.83a.45.45,0,0,1-.62-.14l-3.7-5.8-3.69,5.8a.47.47,0,0,1-.63.14.45.45,0,0,1-.13-.62l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.42.42,0,0,1,.07.24A.43.43,0,0,1,119.09,129.83Z" fill="#4f4f4f"/>
</g>
<g>
<path d="M104.35,108.93h-10a5,5,0,0,1-5-5V83.68" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M93.67,89.83a.45.45,0,0,1-.62-.14l-3.7-5.8-3.7,5.8a.45.45,0,1,1-.76-.48L89,82.81a.45.45,0,0,1,.76,0l4.08,6.4a.42.42,0,0,1,.07.24A.45.45,0,0,1,93.67,89.83Z" fill="#4f4f4f"/>
</g>
<g>
<rect x="154.78" y="177.58" width="110" height="25" fill="#fff"/>
<rect x="154.78" y="177.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="164.78" y1="162.89" x2="164.78" y2="171.94" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="161.04 170.85 164.78 177.32 168.52 170.85 161.04 170.85" fill="#005ba1"/>
</g>
<g>
<path d="M154.78,189.24h-10a5,5,0,0,1-5-5V164" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M144.09,170.15a.45.45,0,0,1-.62-.14l-3.7-5.81L136.08,170a.45.45,0,1,1-.76-.49l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.46.46,0,0,1-.14.63Z" fill="#4f4f4f"/>
</g>
<g>
<rect x="179.78" y="217.27" width="110" height="25" fill="#fff"/>
<rect x="179.78" y="217.27" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="189.78" y1="202.58" x2="189.78" y2="211.63" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="186.04 210.53 189.78 217.01 193.52 210.53 186.04 210.53" fill="#005ba1"/>
</g>
<g>
<path d="M179.78,228.93h-10a5,5,0,0,1-5-5V203.68" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M169.09,209.83a.45.45,0,0,1-.62-.14l-3.7-5.8-3.69,5.8a.47.47,0,0,1-.63.14.45.45,0,0,1-.13-.62l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.42.42,0,0,1,.07.24A.43.43,0,0,1,169.09,209.83Z" fill="#4f4f4f"/>
</g>
<g>
<rect x="204.78" y="257.58" width="110" height="25" fill="#fff"/>
<rect x="204.78" y="257.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="214.78" y1="242.89" x2="214.78" y2="251.94" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="211.04 250.85 214.78 257.32 218.52 250.85 211.04 250.85" fill="#005ba1"/>
</g>
<g>
<path d="M204.78,269.24h-10a5,5,0,0,1-5-5V244" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M194.09,250.15a.45.45,0,0,1-.62-.14l-3.7-5.81L186.08,250a.45.45,0,1,1-.76-.49l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.46.46,0,0,1-.14.63Z" fill="#4f4f4f"/>
</g>
<g>
<rect x="229.78" y="297.89" width="110" height="25" fill="#fff"/>
<rect x="229.78" y="297.89" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="239.78" y1="283.21" x2="239.78" y2="292.26" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="236.04 291.16 239.78 297.64 243.52 291.16 236.04 291.16" fill="#005ba1"/>
</g>
<g>
<path d="M229.78,309.55h-10a5,5,0,0,1-5-5V284.31" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M219.09,290.46a.45.45,0,0,1-.62-.14l-3.7-5.81-3.69,5.81a.47.47,0,0,1-.63.14.45.45,0,0,1-.13-.62l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.42.42,0,0,1,.07.24A.44.44,0,0,1,219.09,290.46Z" fill="#4f4f4f"/>
</g>
<g>
<rect x="254.78" y="337.58" width="110" height="25" fill="#fff"/>
<rect x="254.78" y="337.58" width="110" height="25" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<rect x="386.35" y="337.08" width="90" height="25.5" fill="#fff"/>
<rect x="386.35" y="337.08" width="90" height="25.5" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<rect x="491.35" y="337.08" width="90" height="25.5" fill="#fff"/>
<rect x="491.35" y="337.08" width="90" height="25.5" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<rect x="606.35" y="337.08" width="90" height="25.5" fill="#fff"/>
<rect x="606.35" y="337.08" width="90" height="25.5" fill="#5ea0ef" opacity="0.5"/>
</g>
<g>
<line x1="264.78" y1="322.89" x2="264.78" y2="331.94" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="261.04 330.85 264.78 337.32 268.52 330.85 261.04 330.85" fill="#005ba1"/>
</g>
<g>
<line x1="476.92" y1="349.24" x2="485.97" y2="349.24" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="484.87 352.98 491.35 349.24 484.87 345.5 484.87 352.98" fill="#005ba1"/>
</g>
<g>
<line x1="364.78" y1="349.24" x2="380.97" y2="349.24" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="379.87 352.98 386.35 349.24 379.87 345.5 379.87 352.98" fill="#005ba1"/>
</g>
<g>
<line x1="581.35" y1="349.24" x2="600.97" y2="349.24" fill="none" stroke="#005ba1" stroke-miterlimit="10"/>
<polygon points="599.87 352.98 606.35 349.24 599.87 345.5 599.87 352.98" fill="#005ba1"/>
</g>
<g>
<path d="M254.78,349.24h-10a5,5,0,0,1-5-5V324" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M244.09,330.15a.45.45,0,0,1-.62-.14l-3.7-5.81L236.08,330a.45.45,0,1,1-.76-.49l4.07-6.4a.47.47,0,0,1,.38-.21.44.44,0,0,1,.38.21l4.08,6.4a.46.46,0,0,1-.14.63Z" fill="#4f4f4f"/>
</g>
<g>
<path d="M432.92,362.58v15a5,5,0,0,1-5,5H269.78a5,5,0,0,1-5-5V362.33" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M269.1,368.48a.45.45,0,0,1-.63-.13l-3.69-5.81-3.7,5.81a.44.44,0,0,1-.62.13.45.45,0,0,1-.14-.62l4.08-6.4a.44.44,0,0,1,.38-.21.47.47,0,0,1,.38.21l4.07,6.4a.46.46,0,0,1-.13.62Z" fill="#4f4f4f"/>
</g>
<g>
<path d="M656.35,362.08v15.5a5,5,0,0,1-5,5h-110a5,5,0,0,1-5-5V362.33" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M540.67,368.48a.44.44,0,0,1-.62-.13l-3.7-5.81-3.7,5.81a.44.44,0,0,1-.62.13.45.45,0,0,1-.14-.62l4.08-6.4a.45.45,0,0,1,.76,0l4.08,6.4a.44.44,0,0,1,.07.24A.45.45,0,0,1,540.67,368.48Z" fill="#4f4f4f"/>
</g>
<g>
<path d="M536.35,337.58v-10a5,5,0,0,0-5-5H437.92a5,5,0,0,0-5,5v8.92" fill="none" stroke="#4f4f4f" stroke-miterlimit="10"/>
<path d="M428.6,330.35a.45.45,0,0,1,.63.13l3.69,5.81,3.7-5.81a.44.44,0,0,1,.62-.13.45.45,0,0,1,.14.62l-4.08,6.4a.44.44,0,0,1-.38.21.47.47,0,0,1-.38-.21l-4.07-6.4a.46.46,0,0,1,.13-.62Z" fill="#4f4f4f"/>
</g>
</g>
<g id="Text">
<g>
<path d="M75.51,16.47H74.34l-1.41-2.35a5,5,0,0,0-.37-.56,2.27,2.27,0,0,0-.38-.38,1.07,1.07,0,0,0-.41-.21,1.49,1.49,0,0,0-.49-.07h-.81v3.57h-1V8.07H72a3.64,3.64,0,0,1,1,.14,2.23,2.23,0,0,1,.81.42,1.84,1.84,0,0,1,.53.7,2.28,2.28,0,0,1,.2,1,2.53,2.53,0,0,1-.13.8,2.15,2.15,0,0,1-.38.66,2.37,2.37,0,0,1-.59.49,2.82,2.82,0,0,1-.77.31v0a2.13,2.13,0,0,1,.37.22,2.56,2.56,0,0,1,.3.28c.09.11.18.24.27.37s.2.3.31.49ZM70.47,9v3h1.34a2.15,2.15,0,0,0,.68-.11,1.61,1.61,0,0,0,.54-.32,1.57,1.57,0,0,0,.36-.51,1.83,1.83,0,0,0,.13-.68,1.32,1.32,0,0,0-.44-1.05A1.88,1.88,0,0,0,71.82,9Z" fill="#1e1e1e"/>
<path d="M81,13.71H76.77a2.19,2.19,0,0,0,.54,1.55,1.86,1.86,0,0,0,1.42.55,3,3,0,0,0,1.86-.67V16a3.52,3.52,0,0,1-2.09.57,2.54,2.54,0,0,1-2-.81,3.33,3.33,0,0,1-.73-2.3,3.29,3.29,0,0,1,.8-2.29,2.56,2.56,0,0,1,2-.88,2.28,2.28,0,0,1,1.82.76A3.19,3.19,0,0,1,81,13.21Zm-1-.81a2,2,0,0,0-.4-1.3,1.4,1.4,0,0,0-1.1-.46,1.52,1.52,0,0,0-1.15.49,2.16,2.16,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
<path d="M87.58,19.23h-1V15.44h0a2.14,2.14,0,0,1-2,1.17,2.25,2.25,0,0,1-1.81-.8,3.27,3.27,0,0,1-.68-2.2,3.57,3.57,0,0,1,.75-2.38,2.48,2.48,0,0,1,2-.9,1.89,1.89,0,0,1,1.78,1h0v-.84h1Zm-1-5.46V12.9a1.73,1.73,0,0,0-.49-1.25,1.61,1.61,0,0,0-1.22-.51,1.66,1.66,0,0,0-1.37.64A2.85,2.85,0,0,0,83,13.59a2.48,2.48,0,0,0,.49,1.63,1.55,1.55,0,0,0,1.25.59,1.73,1.73,0,0,0,1.35-.58A2.17,2.17,0,0,0,86.62,13.77Z" fill="#1e1e1e"/>
<path d="M94.38,16.47h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.14-.85-2.14-2.55V10.47h.95v3.44c0,1.26.48,1.9,1.45,1.9A1.49,1.49,0,0,0,93,15.29a2,2,0,0,0,.45-1.36V10.47h1Z" fill="#1e1e1e"/>
<path d="M101.14,13.71H96.9a2.28,2.28,0,0,0,.54,1.55,1.87,1.87,0,0,0,1.42.55,3,3,0,0,0,1.86-.67V16a3.5,3.5,0,0,1-2.09.57,2.56,2.56,0,0,1-2-.81,3.37,3.37,0,0,1-.73-2.3,3.29,3.29,0,0,1,.8-2.29,2.58,2.58,0,0,1,2-.88,2.27,2.27,0,0,1,1.82.76,3.2,3.2,0,0,1,.65,2.12Zm-1-.81a1.91,1.91,0,0,0-.4-1.3,1.39,1.39,0,0,0-1.1-.46,1.52,1.52,0,0,0-1.15.49,2.22,2.22,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
<path d="M102.23,16.26v-1a2.88,2.88,0,0,0,1.73.58c.84,0,1.26-.29,1.26-.85a.7.7,0,0,0-.11-.41,1,1,0,0,0-.29-.29,2.18,2.18,0,0,0-.43-.23l-.54-.22a5.16,5.16,0,0,1-.7-.32,1.74,1.74,0,0,1-.5-.36,1.29,1.29,0,0,1-.31-.46,1.57,1.57,0,0,1-.1-.6,1.44,1.44,0,0,1,.19-.75,1.58,1.58,0,0,1,.52-.54,2.45,2.45,0,0,1,.73-.34,3.49,3.49,0,0,1,.86-.11,3.41,3.41,0,0,1,1.39.27v1a2.78,2.78,0,0,0-1.52-.43,1.83,1.83,0,0,0-.49.06,1.06,1.06,0,0,0-.37.18.66.66,0,0,0-.24.26.7.7,0,0,0-.09.35.8.8,0,0,0,.09.39.82.82,0,0,0,.25.28,1.67,1.67,0,0,0,.4.22l.53.22a6.85,6.85,0,0,1,.71.31,2.44,2.44,0,0,1,.54.37,1.41,1.41,0,0,1,.35.46,1.54,1.54,0,0,1,.12.63,1.5,1.5,0,0,1-.2.77,1.74,1.74,0,0,1-.53.55,2.43,2.43,0,0,1-.75.32,3.76,3.76,0,0,1-.9.1A3.49,3.49,0,0,1,102.23,16.26Z" fill="#1e1e1e"/>
<path d="M110.47,16.41a1.82,1.82,0,0,1-.9.19c-1,0-1.58-.58-1.58-1.76V11.29h-1v-.82h1V9l1-.31v1.77h1.52v.82H109v3.38a1.41,1.41,0,0,0,.21.87.82.82,0,0,0,.68.25,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M14.05,55.66H12.88L11.47,53.3a4.53,4.53,0,0,0-.37-.56,2.15,2.15,0,0,0-.37-.37,1.49,1.49,0,0,0-.41-.22,1.89,1.89,0,0,0-.5-.07H9v3.58H8v-8.4h2.51a3.63,3.63,0,0,1,1,.13,2.59,2.59,0,0,1,.81.42,2,2,0,0,1,.54.7A2.61,2.61,0,0,1,13,50.3a2,2,0,0,1-.38.65,2.13,2.13,0,0,1-.58.49,3.12,3.12,0,0,1-.77.32v0a1.55,1.55,0,0,1,.36.21,1.64,1.64,0,0,1,.3.29,4,4,0,0,1,.28.37l.31.48ZM9,48.15v3h1.34a1.87,1.87,0,0,0,.68-.11,1.46,1.46,0,0,0,.54-.32,1.32,1.32,0,0,0,.36-.51,1.6,1.6,0,0,0,.13-.67,1.35,1.35,0,0,0-.44-1.06,1.88,1.88,0,0,0-1.26-.37Z" fill="#1e1e1e"/>
<path d="M19.55,52.9H15.31a2.3,2.3,0,0,0,.54,1.55,1.87,1.87,0,0,0,1.42.54,2.91,2.91,0,0,0,1.86-.67v.9A3.43,3.43,0,0,1,17,55.8,2.53,2.53,0,0,1,15,55a3.35,3.35,0,0,1-.72-2.3,3.28,3.28,0,0,1,.79-2.28,2.54,2.54,0,0,1,2-.88,2.24,2.24,0,0,1,1.82.76,3.17,3.17,0,0,1,.65,2.11Zm-1-.82a1.9,1.9,0,0,0-.4-1.29,1.36,1.36,0,0,0-1.1-.46,1.56,1.56,0,0,0-1.15.48,2.25,2.25,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
<path d="M20.64,55.44v-1a2.82,2.82,0,0,0,1.73.58q1.26,0,1.26-.84a.68.68,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,2.86,2.86,0,0,0-.43-.23L22.26,53c-.26-.11-.5-.21-.7-.32a2.22,2.22,0,0,1-.5-.36,1.38,1.38,0,0,1-.31-.46,1.66,1.66,0,0,1-.1-.61,1.38,1.38,0,0,1,.19-.74,1.7,1.7,0,0,1,.52-.55,2.21,2.21,0,0,1,.73-.33,3.07,3.07,0,0,1,.86-.11,3.41,3.41,0,0,1,1.39.27v1a2.7,2.7,0,0,0-1.52-.43,1.88,1.88,0,0,0-.49.06,1.26,1.26,0,0,0-.37.17.78.78,0,0,0-.24.27.67.67,0,0,0-.09.34.75.75,0,0,0,.09.39.83.83,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21a6,6,0,0,1,.71.32,2,2,0,0,1,.54.36,1.35,1.35,0,0,1,.35.47,1.46,1.46,0,0,1,.12.62,1.51,1.51,0,0,1-.2.78,1.68,1.68,0,0,1-.52.54,2.26,2.26,0,0,1-.76.32,3.76,3.76,0,0,1-.9.11A3.37,3.37,0,0,1,20.64,55.44Z" fill="#1e1e1e"/>
<path d="M27.08,54.79h0v3.63h-1V49.66h1v1.05h0a2.27,2.27,0,0,1,2.07-1.19,2.2,2.2,0,0,1,1.81.8,3.31,3.31,0,0,1,.65,2.16,3.69,3.69,0,0,1-.73,2.41,2.43,2.43,0,0,1-2,.91A2,2,0,0,1,27.08,54.79Zm0-2.42v.84a1.78,1.78,0,0,0,.49,1.26,1.72,1.72,0,0,0,2.59-.15,3,3,0,0,0,.5-1.86,2.49,2.49,0,0,0-.46-1.57,1.56,1.56,0,0,0-1.26-.56,1.7,1.7,0,0,0-1.35.58A2.15,2.15,0,0,0,27.05,52.37Z" fill="#1e1e1e"/>
<path d="M35.66,55.8A2.79,2.79,0,0,1,33.53,55a3.12,3.12,0,0,1-.79-2.23,3.23,3.23,0,0,1,.82-2.36,3,3,0,0,1,2.24-.85,2.68,2.68,0,0,1,2.09.82,3.29,3.29,0,0,1,.75,2.29,3.21,3.21,0,0,1-.81,2.3A2.82,2.82,0,0,1,35.66,55.8Zm.07-5.47a1.84,1.84,0,0,0-1.47.63,2.58,2.58,0,0,0-.54,1.73,2.46,2.46,0,0,0,.55,1.69,1.85,1.85,0,0,0,1.46.61,1.77,1.77,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.74,1.74,0,0,0,35.73,50.33Z" fill="#1e1e1e"/>
<path d="M45.16,55.66h-1V52.24c0-1.28-.47-1.91-1.4-1.91a1.52,1.52,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.18,2.18,0,0,1,2-1.13,1.85,1.85,0,0,1,1.51.63A2.84,2.84,0,0,1,45.16,52Z" fill="#1e1e1e"/>
<path d="M46.61,55.44v-1a2.82,2.82,0,0,0,1.73.58q1.26,0,1.26-.84a.76.76,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,2.86,2.86,0,0,0-.43-.23L48.23,53a6.76,6.76,0,0,1-.7-.32,2.22,2.22,0,0,1-.5-.36,1.38,1.38,0,0,1-.31-.46,1.66,1.66,0,0,1-.1-.61,1.38,1.38,0,0,1,.19-.74,1.7,1.7,0,0,1,.52-.55,2.21,2.21,0,0,1,.73-.33,3.07,3.07,0,0,1,.86-.11,3.41,3.41,0,0,1,1.39.27v1a2.7,2.7,0,0,0-1.52-.43,1.83,1.83,0,0,0-.49.06,1.26,1.26,0,0,0-.37.17.62.62,0,0,0-.24.27.67.67,0,0,0-.09.34.75.75,0,0,0,.09.39.74.74,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21a6.81,6.81,0,0,1,.71.32,2.42,2.42,0,0,1,.54.36,1.35,1.35,0,0,1,.35.47,1.46,1.46,0,0,1,.12.62,1.51,1.51,0,0,1-.2.78,1.61,1.61,0,0,1-.53.54,2.21,2.21,0,0,1-.75.32,3.82,3.82,0,0,1-.9.11A3.37,3.37,0,0,1,46.61,55.44Z" fill="#1e1e1e"/>
<path d="M56.88,52.9H52.65a2.21,2.21,0,0,0,.54,1.55A1.85,1.85,0,0,0,54.6,55a2.94,2.94,0,0,0,1.87-.67v.9a3.45,3.45,0,0,1-2.09.58,2.52,2.52,0,0,1-2-.82,3.81,3.81,0,0,1,.07-4.58,2.53,2.53,0,0,1,2-.88,2.25,2.25,0,0,1,1.82.76,3.17,3.17,0,0,1,.64,2.11Zm-1-.82a2,2,0,0,0-.4-1.29,1.37,1.37,0,0,0-1.1-.46,1.55,1.55,0,0,0-1.15.48,2.19,2.19,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M93.79,74.58H89.34v-8.4H93.6v.89H90.32v2.79h3v.89h-3v2.94h3.47Z" fill="#1e1e1e"/>
<path d="M99.66,68.58l-2,3,2,3H98.51l-1.18-2c-.08-.12-.16-.27-.27-.45h0l-.27.45-1.21,2h-1.1l2-2.94-2-3.06h1.11l1.16,2c.09.15.17.31.26.47h0l1.5-2.52Z" fill="#1e1e1e"/>
<path d="M104.88,74.3a3.14,3.14,0,0,1-1.65.42,2.72,2.72,0,0,1-2.07-.84,3,3,0,0,1-.78-2.16,3.3,3.3,0,0,1,.85-2.38,3,3,0,0,1,2.26-.9,3.27,3.27,0,0,1,1.4.29v1a2.4,2.4,0,0,0-1.43-.47,1.92,1.92,0,0,0-1.51.66,2.5,2.5,0,0,0-.59,1.73,2.36,2.36,0,0,0,.55,1.66,1.91,1.91,0,0,0,1.49.61,2.43,2.43,0,0,0,1.48-.52Z" fill="#1e1e1e"/>
<path d="M111.15,71.82h-4.24a2.3,2.3,0,0,0,.54,1.55,1.88,1.88,0,0,0,1.42.54,2.89,2.89,0,0,0,1.86-.67v.91a3.47,3.47,0,0,1-2.09.57,2.51,2.51,0,0,1-2-.82,3.83,3.83,0,0,1,.06-4.58,2.54,2.54,0,0,1,2-.88,2.26,2.26,0,0,1,1.83.76,3.18,3.18,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.37,1.37,0,0,0-1.1-.46,1.57,1.57,0,0,0-1.16.48,2.24,2.24,0,0,0-.58,1.28Z" fill="#1e1e1e"/>
<path d="M113.59,73.71h0v3.63h-1V68.58h1v1h0a2.27,2.27,0,0,1,2.07-1.19,2.2,2.2,0,0,1,1.81.8,3.31,3.31,0,0,1,.65,2.16,3.69,3.69,0,0,1-.73,2.41,2.43,2.43,0,0,1-2,.91A2,2,0,0,1,113.59,73.71Zm0-2.42v.84a1.78,1.78,0,0,0,.49,1.26,1.72,1.72,0,0,0,2.59-.15,3,3,0,0,0,.5-1.85,2.45,2.45,0,0,0-.46-1.57,1.54,1.54,0,0,0-1.26-.57,1.7,1.7,0,0,0-1.35.58A2.15,2.15,0,0,0,113.56,71.29Z" fill="#1e1e1e"/>
<path d="M122.44,74.52a1.76,1.76,0,0,1-.89.19c-1.06,0-1.58-.59-1.58-1.76V69.4h-1v-.82h1V67.11l1-.31v1.78h1.51v.82h-1.51v3.38a1.37,1.37,0,0,0,.21.86.8.8,0,0,0,.67.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M124.22,67.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.64.64,0,0,1,.45.18.6.6,0,0,1,.18.44.59.59,0,0,1-.18.44A.61.61,0,0,1,124.22,67.06Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M129.14,74.72a2.76,2.76,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.23,3.23,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.68,2.68,0,0,1,2.09.82,3.76,3.76,0,0,1-.05,4.6A2.88,2.88,0,0,1,129.14,74.72Zm.07-5.47a1.84,1.84,0,0,0-1.47.63,2.63,2.63,0,0,0-.53,1.73,2.47,2.47,0,0,0,.54,1.69,1.85,1.85,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.73,1.73,0,0,0,129.21,69.25Z" fill="#1e1e1e"/>
<path d="M138.64,74.58h-1V71.16c0-1.28-.46-1.91-1.39-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.48,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.84,1.84,0,0,1,1.5.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
<path d="M146.9,74.58h-1V70.75h-4.34v3.83h-1v-8.4h1v3.68h4.34V66.18h1Z" fill="#1e1e1e"/>
<path d="M153.27,74.58h-1v-.94h0a2.2,2.2,0,0,1-3.25.61,1.68,1.68,0,0,1-.5-1.26c0-1.13.66-1.78,2-2l1.8-.25c0-1-.41-1.53-1.24-1.53a2.94,2.94,0,0,0-2,.74V69a3.76,3.76,0,0,1,2-.56c1.41,0,2.12.74,2.12,2.24Zm-1-3-1.45.2a2.59,2.59,0,0,0-1,.33,1,1,0,0,0-.34.85.9.9,0,0,0,.32.71,1.16,1.16,0,0,0,.83.28,1.55,1.55,0,0,0,1.18-.5,1.8,1.8,0,0,0,.47-1.27Z" fill="#1e1e1e"/>
<path d="M160.06,74.58h-1V71.16c0-1.28-.47-1.91-1.4-1.91a1.52,1.52,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.17,2.17,0,0,1,2-1.14,1.85,1.85,0,0,1,1.51.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
<path d="M167,74.58h-1v-1h0a2.42,2.42,0,0,1-3.87.35,3.28,3.28,0,0,1-.68-2.19,3.57,3.57,0,0,1,.75-2.38,2.48,2.48,0,0,1,2-.9,1.93,1.93,0,0,1,1.8,1h0V65.7h1Zm-1-2.71V71a1.73,1.73,0,0,0-.48-1.23,1.78,1.78,0,0,0-2.6.14,2.81,2.81,0,0,0-.51,1.78,2.51,2.51,0,0,0,.49,1.64,1.57,1.57,0,0,0,1.3.6,1.64,1.64,0,0,0,1.3-.58A2.17,2.17,0,0,0,166,71.87Z" fill="#1e1e1e"/>
<path d="M169.9,74.58h-1V65.7h1Z" fill="#1e1e1e"/>
<path d="M176.66,71.82h-4.23a2.21,2.21,0,0,0,.54,1.55,1.85,1.85,0,0,0,1.42.54,2.93,2.93,0,0,0,1.86-.67v.91a3.52,3.52,0,0,1-2.09.57,2.52,2.52,0,0,1-2-.82,3.81,3.81,0,0,1,.07-4.58,2.53,2.53,0,0,1,2-.88,2.25,2.25,0,0,1,1.82.76,3.18,3.18,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.36,1.36,0,0,0-1.1-.46,1.55,1.55,0,0,0-1.15.48,2.19,2.19,0,0,0-.59,1.28Z" fill="#1e1e1e"/>
<path d="M181.25,69.55a1.18,1.18,0,0,0-.73-.19,1.23,1.23,0,0,0-1,.58,2.72,2.72,0,0,0-.41,1.58v3.06h-1v-6h1v1.24h0a2.07,2.07,0,0,1,.63-1,1.4,1.4,0,0,1,.94-.36,1.5,1.5,0,0,1,.58.09Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M153.53,113.58h-1v-3.83H148.2v3.83h-1v-8.4h1v3.68h4.35v-3.68h1Z" fill="#1e1e1e"/>
<path d="M155.34,113.24v-1.16a2.86,2.86,0,0,0,.48.32,3.8,3.8,0,0,0,.59.23,3.39,3.39,0,0,0,.61.15,3.09,3.09,0,0,0,.58.06,2.31,2.31,0,0,0,1.36-.34,1.17,1.17,0,0,0,.44-1,1.1,1.1,0,0,0-.15-.59,1.6,1.6,0,0,0-.41-.46,3.91,3.91,0,0,0-.62-.4l-.78-.4c-.29-.15-.57-.3-.82-.45a3.85,3.85,0,0,1-.66-.51,2,2,0,0,1-.35-2.44,2.1,2.1,0,0,1,.66-.7,3,3,0,0,1,.93-.41,4.29,4.29,0,0,1,1.07-.13,4.08,4.08,0,0,1,1.81.3v1.1a3.29,3.29,0,0,0-1.91-.51,3.47,3.47,0,0,0-.64.06,2,2,0,0,0-.58.22,1.56,1.56,0,0,0-.41.4,1.06,1.06,0,0,0-.15.58,1.22,1.22,0,0,0,.12.56,1.28,1.28,0,0,0,.35.43,3.75,3.75,0,0,0,.57.37l.78.4c.3.15.58.31.85.47a3.59,3.59,0,0,1,.71.54,2.27,2.27,0,0,1,.49.67,1.84,1.84,0,0,1,.18.83,2.15,2.15,0,0,1-.25,1.05,2,2,0,0,1-.65.7,2.94,2.94,0,0,1-1,.39,5.15,5.15,0,0,1-1.13.12l-.5,0c-.19,0-.39-.06-.59-.1a4.45,4.45,0,0,1-.58-.15A2,2,0,0,1,155.34,113.24Z" fill="#1e1e1e"/>
<path d="M167.08,106.07h-2.43v7.51h-1v-7.51h-2.42v-.89h5.83Z" fill="#1e1e1e"/>
<path d="M167.77,113.24v-1.16a2.23,2.23,0,0,0,.48.32,4.51,4.51,0,0,0,1.2.38,3.09,3.09,0,0,0,.58.06,2.27,2.27,0,0,0,1.35-.34,1.15,1.15,0,0,0,.45-1,1.1,1.1,0,0,0-.15-.59,1.76,1.76,0,0,0-.41-.46,4.43,4.43,0,0,0-.62-.4l-.78-.4c-.29-.15-.57-.3-.82-.45a3.2,3.2,0,0,1-.66-.51,2.1,2.1,0,0,1-.45-.62,2.07,2.07,0,0,1-.16-.82,2,2,0,0,1,.25-1,2.13,2.13,0,0,1,.67-.7,3,3,0,0,1,.93-.41,4.29,4.29,0,0,1,1.07-.13,4.08,4.08,0,0,1,1.81.3v1.1a3.29,3.29,0,0,0-1.91-.51,3.47,3.47,0,0,0-.64.06,2,2,0,0,0-.58.22,1.42,1.42,0,0,0-.41.4,1.07,1.07,0,0,0-.16.58,1.22,1.22,0,0,0,.12.56,1.31,1.31,0,0,0,.36.43,3.31,3.31,0,0,0,.57.37c.22.12.48.26.78.4s.58.31.85.47a3.59,3.59,0,0,1,.71.54,2.42,2.42,0,0,1,.48.67,1.84,1.84,0,0,1,.18.83,2.14,2.14,0,0,1-.24,1.05,1.93,1.93,0,0,1-.66.7,2.8,2.8,0,0,1-1,.39,5.24,5.24,0,0,1-1.14.12l-.49,0c-.19,0-.39-.06-.6-.1a4.74,4.74,0,0,1-.57-.15A2,2,0,0,1,167.77,113.24Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M147.86,153.26h-1v-3.83h-4.35v3.83h-1v-8.4h1v3.69h4.35v-3.69h1Z" fill="#1e1e1e"/>
<path d="M152.72,153.21a1.87,1.87,0,0,1-.89.18c-1.06,0-1.58-.58-1.58-1.75v-3.55h-1v-.83h1V145.8l1-.31v1.77h1.51v.83h-1.51v3.38a1.37,1.37,0,0,0,.21.86.81.81,0,0,0,.68.26,1,1,0,0,0,.62-.2Z" fill="#1e1e1e"/>
<path d="M156.79,153.21a2,2,0,0,1-.9.18c-1,0-1.57-.58-1.57-1.75v-3.55h-1v-.83h1V145.8l1-.31v1.77h1.51v.83h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M159.06,152.4h0V156h-1v-8.76h1v1.06h0a2.26,2.26,0,0,1,2.07-1.2,2.19,2.19,0,0,1,1.81.81,3.37,3.37,0,0,1,.65,2.16,3.71,3.71,0,0,1-.73,2.41,2.46,2.46,0,0,1-2,.91A2,2,0,0,1,159.06,152.4Zm0-2.42v.84a1.77,1.77,0,0,0,.49,1.26,1.71,1.71,0,0,0,2.59-.15,3.09,3.09,0,0,0,.5-1.86,2.41,2.41,0,0,0-.47-1.57,1.51,1.51,0,0,0-1.25-.57,1.71,1.71,0,0,0-1.35.59A2.11,2.11,0,0,0,159,150Z" fill="#1e1e1e"/>
<path d="M164.76,153.05v-1a2.9,2.9,0,0,0,1.73.58c.85,0,1.27-.28,1.27-.85a.72.72,0,0,0-.11-.4,1,1,0,0,0-.29-.3,2.27,2.27,0,0,0-.44-.23l-.53-.22a6.87,6.87,0,0,1-.7-.31,2,2,0,0,1-.51-.37,1.41,1.41,0,0,1-.3-.46,1.57,1.57,0,0,1-.11-.6,1.45,1.45,0,0,1,.2-.75,1.65,1.65,0,0,1,.51-.54,2.26,2.26,0,0,1,.74-.33,3,3,0,0,1,.85-.12,3.46,3.46,0,0,1,1.4.27v1a2.74,2.74,0,0,0-1.53-.44,1.75,1.75,0,0,0-.48.06,1.34,1.34,0,0,0-.38.18.85.85,0,0,0-.24.26.82.82,0,0,0-.08.35.74.74,0,0,0,.33.67,1.89,1.89,0,0,0,.4.22l.53.22c.27.1.51.21.72.31a2.44,2.44,0,0,1,.54.37,1.38,1.38,0,0,1,.34.46,1.54,1.54,0,0,1,.12.63,1.47,1.47,0,0,1-.19.77,1.85,1.85,0,0,1-.53.55,2.54,2.54,0,0,1-.75.32,3.82,3.82,0,0,1-.9.11A3.41,3.41,0,0,1,164.76,153.05Z" fill="#1e1e1e"/>
<path d="M176.37,153.26H175.2l-1.41-2.35a5,5,0,0,0-.37-.56,2.69,2.69,0,0,0-.37-.38,1.23,1.23,0,0,0-.41-.21,1.59,1.59,0,0,0-.5-.07h-.81v3.57h-1v-8.4h2.5a3.64,3.64,0,0,1,1,.14,2.23,2.23,0,0,1,.81.42,2,2,0,0,1,.54.7,2.32,2.32,0,0,1,.19,1,2.37,2.37,0,0,1-.13.81,2.22,2.22,0,0,1-.38.65,2.32,2.32,0,0,1-.58.49,3.1,3.1,0,0,1-.77.31v0a1.55,1.55,0,0,1,.36.21,2,2,0,0,1,.3.28,4.17,4.17,0,0,1,.28.38c.09.13.19.3.3.48Zm-5-7.51v3.05h1.34a2.15,2.15,0,0,0,.68-.11,1.61,1.61,0,0,0,.54-.32,1.57,1.57,0,0,0,.36-.51,1.65,1.65,0,0,0,.13-.68,1.32,1.32,0,0,0-.44-1.05,1.88,1.88,0,0,0-1.26-.38Z" fill="#1e1e1e"/>
<path d="M181.87,150.51h-4.24a2.27,2.27,0,0,0,.54,1.54,1.84,1.84,0,0,0,1.42.55,3,3,0,0,0,1.86-.67v.9a3.5,3.5,0,0,1-2.09.58,2.56,2.56,0,0,1-2-.82,3.37,3.37,0,0,1-.73-2.3,3.25,3.25,0,0,1,.8-2.28,2.54,2.54,0,0,1,2-.89,2.24,2.24,0,0,1,1.82.77,3.15,3.15,0,0,1,.65,2.11Zm-1-.82a1.93,1.93,0,0,0-.4-1.29,1.36,1.36,0,0,0-1.1-.47,1.52,1.52,0,0,0-1.15.49,2.22,2.22,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
<path d="M188.44,153.26h-1v-1h0a2.42,2.42,0,0,1-3.87.35,3.32,3.32,0,0,1-.68-2.19,3.6,3.6,0,0,1,.75-2.39,2.48,2.48,0,0,1,2-.9,1.94,1.94,0,0,1,1.8,1h0v-3.72h1Zm-1-2.71v-.88a1.7,1.7,0,0,0-.48-1.23,1.61,1.61,0,0,0-1.22-.51,1.64,1.64,0,0,0-1.38.65,2.79,2.79,0,0,0-.51,1.78,2.56,2.56,0,0,0,.49,1.64,1.57,1.57,0,0,0,1.3.6A1.66,1.66,0,0,0,187,152,2.2,2.2,0,0,0,187.48,150.55Z" fill="#1e1e1e"/>
<path d="M190.88,145.74a.64.64,0,0,1-.44-.17.65.65,0,0,1,0-.9.6.6,0,0,1,.44-.18.64.64,0,0,1,.45.18.61.61,0,0,1,.18.45.6.6,0,0,1-.18.44A.64.64,0,0,1,190.88,145.74Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M196.42,148.24a1.18,1.18,0,0,0-.73-.2,1.22,1.22,0,0,0-1,.58,2.68,2.68,0,0,0-.42,1.59v3h-1v-6h1v1.24h0a2.08,2.08,0,0,1,.62-1,1.49,1.49,0,0,1,.95-.35,1.65,1.65,0,0,1,.57.08Z" fill="#1e1e1e"/>
<path d="M202.29,150.51h-4.24a2.27,2.27,0,0,0,.54,1.54,1.84,1.84,0,0,0,1.42.55,3,3,0,0,0,1.86-.67v.9a3.5,3.5,0,0,1-2.09.58,2.56,2.56,0,0,1-2-.82,3.37,3.37,0,0,1-.73-2.3,3.25,3.25,0,0,1,.8-2.28,2.54,2.54,0,0,1,2-.89,2.26,2.26,0,0,1,1.82.77,3.15,3.15,0,0,1,.65,2.11Zm-1-.82a1.93,1.93,0,0,0-.4-1.29,1.36,1.36,0,0,0-1.1-.47,1.52,1.52,0,0,0-1.15.49,2.22,2.22,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
<path d="M207.83,153a3.1,3.1,0,0,1-1.64.42,2.69,2.69,0,0,1-2.07-.84,3,3,0,0,1-.79-2.16,3.33,3.33,0,0,1,.85-2.39,3,3,0,0,1,2.27-.9,3.09,3.09,0,0,1,1.39.3v1a2.45,2.45,0,0,0-1.43-.47,2,2,0,0,0-1.51.66,2.52,2.52,0,0,0-.59,1.73,2.39,2.39,0,0,0,.56,1.67,1.92,1.92,0,0,0,1.48.61,2.45,2.45,0,0,0,1.48-.52Z" fill="#1e1e1e"/>
<path d="M212.07,153.21a2,2,0,0,1-.9.18c-1,0-1.58-.58-1.58-1.75v-3.55h-1v-.83h1V145.8l1-.31v1.77h1.52v.83h-1.52v3.38a1.37,1.37,0,0,0,.21.86.83.83,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M213.84,145.74a.64.64,0,0,1-.44-.17.65.65,0,0,1,0-.9.6.6,0,0,1,.44-.18.62.62,0,0,1,.63.63.6.6,0,0,1-.18.44A.61.61,0,0,1,213.84,145.74Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M218.76,153.41a2.76,2.76,0,0,1-2.12-.85,3.09,3.09,0,0,1-.79-2.22,3.27,3.27,0,0,1,.82-2.37,3,3,0,0,1,2.23-.85,2.7,2.7,0,0,1,2.1.83,3.27,3.27,0,0,1,.75,2.29,3.23,3.23,0,0,1-.81,2.3A2.84,2.84,0,0,1,218.76,153.41Zm.07-5.48a1.83,1.83,0,0,0-1.46.63,2.59,2.59,0,0,0-.54,1.74,2.46,2.46,0,0,0,.54,1.68,1.87,1.87,0,0,0,1.46.62,1.76,1.76,0,0,0,1.44-.61,2.62,2.62,0,0,0,.5-1.71,2.71,2.71,0,0,0-.5-1.74A1.79,1.79,0,0,0,218.83,147.93Z" fill="#1e1e1e"/>
<path d="M228.27,153.26h-1v-3.42c0-1.27-.47-1.91-1.4-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.86,1.86,0,0,1,1.51.64,2.88,2.88,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M183,193.24v-1.16a2.86,2.86,0,0,0,.48.32,3.8,3.8,0,0,0,.59.23,3.3,3.3,0,0,0,.62.15,2.91,2.91,0,0,0,.57.06,2.29,2.29,0,0,0,1.36-.34,1.14,1.14,0,0,0,.44-1,1.2,1.2,0,0,0-.14-.59,1.64,1.64,0,0,0-.42-.46,3.91,3.91,0,0,0-.62-.4l-.78-.4-.82-.45a3.85,3.85,0,0,1-.66-.51,2.06,2.06,0,0,1-.35-2.44,2.21,2.21,0,0,1,.66-.7,3,3,0,0,1,.93-.41,4.29,4.29,0,0,1,1.07-.13,4,4,0,0,1,1.81.3v1.1a3.29,3.29,0,0,0-1.91-.51,3.47,3.47,0,0,0-.64.06,1.83,1.83,0,0,0-.57.22,1.31,1.31,0,0,0-.41.4,1,1,0,0,0-.16.58,1.22,1.22,0,0,0,.12.56,1.41,1.41,0,0,0,.35.43,3.75,3.75,0,0,0,.57.37l.78.4c.3.15.59.31.85.47a3.59,3.59,0,0,1,.71.54,2.27,2.27,0,0,1,.49.67,1.84,1.84,0,0,1,.18.83,2.15,2.15,0,0,1-.25,1,2,2,0,0,1-.65.7,2.94,2.94,0,0,1-1,.39,5.15,5.15,0,0,1-1.13.12l-.49,0c-.2,0-.4-.06-.6-.1a4.45,4.45,0,0,1-.58-.15A2,2,0,0,1,183,193.24Z" fill="#1e1e1e"/>
<path d="M192,193.52a1.82,1.82,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V188.4h-1v-.82h1v-1.47l1-.31v1.78H192v.82h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M197.6,193.58h-1v-.94h0a2.2,2.2,0,0,1-3.25.61,1.68,1.68,0,0,1-.5-1.26c0-1.13.66-1.78,2-2l1.8-.25c0-1-.42-1.53-1.24-1.53a2.92,2.92,0,0,0-2,.74v-1a3.76,3.76,0,0,1,2-.56c1.41,0,2.12.74,2.12,2.24Zm-1-3-1.45.2a2.59,2.59,0,0,0-1,.33,1,1,0,0,0-.34.85.92.92,0,0,0,.31.71,1.2,1.2,0,0,0,.84.28,1.55,1.55,0,0,0,1.18-.5,1.8,1.8,0,0,0,.47-1.27Z" fill="#1e1e1e"/>
<path d="M202.19,193.52a1.8,1.8,0,0,1-.9.19c-1,0-1.57-.59-1.57-1.76V188.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M204,186.06a.59.59,0,0,1-.44-.18.61.61,0,0,1-.18-.45.6.6,0,0,1,.18-.44.59.59,0,0,1,.44-.18.6.6,0,0,1,.44.18.58.58,0,0,1,.19.44.56.56,0,0,1-.19.44A.58.58,0,0,1,204,186.06Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M210.47,193.3a3.1,3.1,0,0,1-1.64.42,2.69,2.69,0,0,1-2.07-.84,3,3,0,0,1-.79-2.16,3.3,3.3,0,0,1,.85-2.38,3,3,0,0,1,2.27-.9,3.23,3.23,0,0,1,1.39.29v1a2.38,2.38,0,0,0-1.43-.47,2,2,0,0,0-1.51.66,2.5,2.5,0,0,0-.59,1.73,2.36,2.36,0,0,0,.56,1.66,1.89,1.89,0,0,0,1.48.61,2.39,2.39,0,0,0,1.48-.52Z" fill="#1e1e1e"/>
<path d="M219.6,186.07h-3.28V189h3v.89h-3v3.72h-1v-8.4h4.26Z" fill="#1e1e1e"/>
<path d="M221.56,186.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.61.61,0,0,1,.45.18.6.6,0,0,1,.18.44.63.63,0,0,1-.63.63Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M224.94,193.58h-1V184.7h1Z" fill="#1e1e1e"/>
<path d="M231.7,190.82h-4.23a2.21,2.21,0,0,0,.54,1.55,1.85,1.85,0,0,0,1.42.54,2.93,2.93,0,0,0,1.86-.67v.91a3.52,3.52,0,0,1-2.09.57,2.52,2.52,0,0,1-2-.82,3.81,3.81,0,0,1,.07-4.58,2.53,2.53,0,0,1,2-.88,2.25,2.25,0,0,1,1.82.76,3.18,3.18,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.36,1.36,0,0,0-1.1-.46,1.55,1.55,0,0,0-1.15.48,2.19,2.19,0,0,0-.59,1.28Z" fill="#1e1e1e"/>
<path d="M232.79,193.36v-1a2.84,2.84,0,0,0,1.73.58c.85,0,1.27-.28,1.27-.84a.76.76,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,3,3,0,0,0-.44-.23l-.53-.21a6.76,6.76,0,0,1-.7-.32,2.55,2.55,0,0,1-.51-.36,1.52,1.52,0,0,1-.3-.46,1.66,1.66,0,0,1-.1-.61,1.38,1.38,0,0,1,.19-.74,1.7,1.7,0,0,1,.52-.55,2.21,2.21,0,0,1,.73-.33,3.59,3.59,0,0,1,2.25.16v1a2.71,2.71,0,0,0-1.53-.43,1.75,1.75,0,0,0-.48.06,1.15,1.15,0,0,0-.37.17.68.68,0,0,0-.24.27.67.67,0,0,0-.09.34.75.75,0,0,0,.09.39.8.8,0,0,0,.24.28,1.91,1.91,0,0,0,.4.23l.54.21c.26.11.5.21.71.32a2.82,2.82,0,0,1,.54.36,1.41,1.41,0,0,1,.34.47,1.46,1.46,0,0,1,.12.62,1.5,1.5,0,0,1-.19.78,1.61,1.61,0,0,1-.53.54,2.3,2.3,0,0,1-.75.32,3.82,3.82,0,0,1-.9.11A3.41,3.41,0,0,1,232.79,193.36Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M221.16,233.26H220l-1.41-2.35a5,5,0,0,0-.37-.56,2.69,2.69,0,0,0-.37-.38,1.23,1.23,0,0,0-.41-.21,1.59,1.59,0,0,0-.5-.07h-.81v3.57h-1v-8.4h2.51a3.62,3.62,0,0,1,1,.14,2.23,2.23,0,0,1,.81.42,2,2,0,0,1,.54.7,2.32,2.32,0,0,1,.19,1,2.37,2.37,0,0,1-.13.81,2,2,0,0,1-.38.65,2.32,2.32,0,0,1-.58.49,3.1,3.1,0,0,1-.77.31v0a1.55,1.55,0,0,1,.36.21,1.58,1.58,0,0,1,.3.28,4.17,4.17,0,0,1,.28.38l.31.48Zm-5-7.51v3.05h1.34a2.15,2.15,0,0,0,.68-.11,1.61,1.61,0,0,0,.54-.32,1.43,1.43,0,0,0,.36-.51,1.65,1.65,0,0,0,.13-.68,1.32,1.32,0,0,0-.44-1.05,1.88,1.88,0,0,0-1.26-.38Z" fill="#1e1e1e"/>
<path d="M224.34,233.41a2.76,2.76,0,0,1-2.12-.85,3.09,3.09,0,0,1-.79-2.22,3.27,3.27,0,0,1,.82-2.37,3,3,0,0,1,2.23-.85,2.7,2.7,0,0,1,2.1.83,3.27,3.27,0,0,1,.75,2.29,3.23,3.23,0,0,1-.81,2.3A2.84,2.84,0,0,1,224.34,233.41Zm.07-5.48a1.83,1.83,0,0,0-1.46.63,2.59,2.59,0,0,0-.54,1.74A2.46,2.46,0,0,0,223,232a1.87,1.87,0,0,0,1.46.62,1.76,1.76,0,0,0,1.44-.61,2.62,2.62,0,0,0,.5-1.71,2.71,2.71,0,0,0-.5-1.74A1.79,1.79,0,0,0,224.41,227.93Z" fill="#1e1e1e"/>
<path d="M233.72,233.26h-1v-.94h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.14-.86-2.14-2.56v-3.59h.95v3.44c0,1.26.49,1.9,1.46,1.9a1.48,1.48,0,0,0,1.15-.52,2,2,0,0,0,.46-1.36v-3.46h1Z" fill="#1e1e1e"/>
<path d="M238.44,233.21a1.92,1.92,0,0,1-.9.18c-1,0-1.57-.58-1.57-1.75v-3.55h-1v-.83h1V225.8l1-.31v1.77h1.51v.83h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M240.22,225.74a.62.62,0,0,1-.44-.17.65.65,0,0,1,0-.9.59.59,0,0,1,.44-.18.6.6,0,0,1,.44.18.58.58,0,0,1,.19.45.58.58,0,0,1-.19.44A.6.6,0,0,1,240.22,225.74Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M247.61,233.26h-1v-3.42c0-1.27-.46-1.91-1.39-1.91a1.52,1.52,0,0,0-1.2.54,2.05,2.05,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.17,2.17,0,0,1,2-1.14,1.86,1.86,0,0,1,1.51.64,2.82,2.82,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
<path d="M254.54,232.78q0,3.32-3.16,3.31a4.31,4.31,0,0,1-1.95-.42v-1a4.06,4.06,0,0,0,1.94.56c1.47,0,2.21-.79,2.21-2.36v-.65h0a2.43,2.43,0,0,1-3.87.35,3.24,3.24,0,0,1-.68-2.15,3.73,3.73,0,0,1,.74-2.43,2.44,2.44,0,0,1,2-.91,2,2,0,0,1,1.8,1h0v-.84h1Zm-1-2.23v-.88a1.73,1.73,0,0,0-.48-1.23,1.6,1.6,0,0,0-1.21-.51,1.68,1.68,0,0,0-1.39.65,2.86,2.86,0,0,0-.5,1.81,2.51,2.51,0,0,0,.48,1.61,1.56,1.56,0,0,0,1.28.6,1.63,1.63,0,0,0,1.31-.58A2.12,2.12,0,0,0,253.58,230.55Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M251.74,273.23a4.89,4.89,0,0,1-2.32.49,3.74,3.74,0,0,1-2.87-1.15,4.26,4.26,0,0,1-1.08-3,4.46,4.46,0,0,1,1.22-3.26,4.08,4.08,0,0,1,3.07-1.24,4.92,4.92,0,0,1,2,.34v1.05a4,4,0,0,0-2-.5,3.08,3.08,0,0,0-2.35,1,3.66,3.66,0,0,0-.9,2.59,3.41,3.41,0,0,0,.85,2.44,2.85,2.85,0,0,0,2.2.92,4.06,4.06,0,0,0,2.19-.57Z" fill="#1e1e1e"/>
<path d="M256.52,273.72a3.66,3.66,0,0,1-2.86-1.18,4.33,4.33,0,0,1-1.08-3.06,4.66,4.66,0,0,1,1.1-3.24,3.83,3.83,0,0,1,3-1.2,3.6,3.6,0,0,1,2.8,1.17,4.37,4.37,0,0,1,1.07,3.06,4.66,4.66,0,0,1-1.09,3.25A3.76,3.76,0,0,1,256.52,273.72Zm.07-7.79a2.73,2.73,0,0,0-2.15.95,4.24,4.24,0,0,0,0,5,2.61,2.61,0,0,0,2.1.95,2.77,2.77,0,0,0,2.18-.91,3.68,3.68,0,0,0,.79-2.52,3.81,3.81,0,0,0-.77-2.57A2.63,2.63,0,0,0,256.59,265.93Z" fill="#1e1e1e"/>
<path d="M268.2,273.58H267l-1.41-2.36c-.13-.21-.25-.4-.37-.56a2.15,2.15,0,0,0-.37-.37,1.49,1.49,0,0,0-.41-.22,2,2,0,0,0-.5-.06h-.81v3.57h-1v-8.4h2.51a3.63,3.63,0,0,1,1,.13,2.59,2.59,0,0,1,.81.42,2,2,0,0,1,.54.7,2.33,2.33,0,0,1,.19,1,2.28,2.28,0,0,1-.13.8,2,2,0,0,1-.38.65,2,2,0,0,1-.58.49,3.12,3.12,0,0,1-.77.32v0a1.55,1.55,0,0,1,.36.21,1.64,1.64,0,0,1,.3.29,4,4,0,0,1,.28.37l.31.48Zm-5-7.51v3h1.34a1.9,1.9,0,0,0,.68-.11,1.46,1.46,0,0,0,.54-.32,1.28,1.28,0,0,0,.36-.51,1.6,1.6,0,0,0,.13-.67,1.32,1.32,0,0,0-.44-1.05,1.83,1.83,0,0,0-1.26-.38Z" fill="#1e1e1e"/>
<path d="M269,273.24v-1.16a2.86,2.86,0,0,0,.48.32,3.8,3.8,0,0,0,.59.23,3.3,3.3,0,0,0,.62.15,2.91,2.91,0,0,0,.57.06,2.31,2.31,0,0,0,1.36-.34,1.17,1.17,0,0,0,.44-1,1.1,1.1,0,0,0-.15-.59,1.6,1.6,0,0,0-.41-.46,3.91,3.91,0,0,0-.62-.4l-.78-.4c-.29-.15-.57-.3-.82-.45a3.85,3.85,0,0,1-.66-.51,2.06,2.06,0,0,1-.35-2.44,2.21,2.21,0,0,1,.66-.7,3,3,0,0,1,.93-.41,4.29,4.29,0,0,1,1.07-.13,4.08,4.08,0,0,1,1.81.3v1.1a3.29,3.29,0,0,0-1.91-.51,3.47,3.47,0,0,0-.64.06,2,2,0,0,0-.58.22,1.56,1.56,0,0,0-.41.4,1.06,1.06,0,0,0-.15.58,1.22,1.22,0,0,0,.12.56,1.28,1.28,0,0,0,.35.43,3.75,3.75,0,0,0,.57.37l.78.4c.3.15.58.31.85.47a3.59,3.59,0,0,1,.71.54,2.27,2.27,0,0,1,.49.67,1.84,1.84,0,0,1,.18.83,2.15,2.15,0,0,1-.25,1.05,2,2,0,0,1-.65.7,2.94,2.94,0,0,1-1,.39,5.15,5.15,0,0,1-1.13.12l-.5,0c-.19,0-.39-.06-.59-.1a4.45,4.45,0,0,1-.58-.15A2,2,0,0,1,269,273.24Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M253.45,313.89h-1.09l-.89-2.35H247.9l-.84,2.35H246l3.22-8.4h1Zm-2.31-3.24-1.32-3.58a3.36,3.36,0,0,1-.12-.56h0a3.13,3.13,0,0,1-.13.56l-1.31,3.58Z" fill="#1e1e1e"/>
<path d="M259.4,313.89h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.15-.85-2.15-2.55v-3.59h1v3.44c0,1.26.48,1.9,1.45,1.9a1.49,1.49,0,0,0,1.16-.52,2,2,0,0,0,.45-1.36v-3.46h1Z" fill="#1e1e1e"/>
<path d="M264.13,313.83a1.82,1.82,0,0,1-.9.19c-1.05,0-1.58-.58-1.58-1.76v-3.55h-1v-.82h1v-1.46l1-.31v1.77h1.51v.82h-1.51v3.38a1.41,1.41,0,0,0,.2.87.82.82,0,0,0,.68.25,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M270.39,313.89h-1v-3.45c0-1.25-.46-1.88-1.39-1.88a1.53,1.53,0,0,0-1.19.54,2,2,0,0,0-.48,1.39v3.4h-1V305h1v3.88h0a2.17,2.17,0,0,1,2-1.14c1.36,0,2,.82,2,2.45Z" fill="#1e1e1e"/>
<path d="M277,311.13h-4.23a2.19,2.19,0,0,0,.54,1.55,1.85,1.85,0,0,0,1.41.55,3,3,0,0,0,1.87-.67v.9a3.52,3.52,0,0,1-2.09.57,2.54,2.54,0,0,1-2-.81,3.34,3.34,0,0,1-.73-2.3,3.29,3.29,0,0,1,.8-2.29,2.55,2.55,0,0,1,2-.88,2.28,2.28,0,0,1,1.82.76,3.19,3.19,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.4,1.4,0,0,0-1.1-.46,1.52,1.52,0,0,0-1.15.49,2.16,2.16,0,0,0-.59,1.27Z" fill="#1e1e1e"/>
<path d="M283.46,313.89h-1v-3.42c0-1.27-.47-1.91-1.4-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.83,1.83,0,0,1,1.51.64,2.86,2.86,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
<path d="M288.05,313.83a1.8,1.8,0,0,1-.9.19c-1,0-1.57-.58-1.57-1.76v-3.55h-1v-.82h1v-1.46l1-.31v1.77h1.51v.82h-1.51v3.38a1.49,1.49,0,0,0,.2.87.83.83,0,0,0,.68.25,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M289.83,306.37a.59.59,0,0,1-.44-.18.57.57,0,0,1-.18-.44.61.61,0,0,1,.18-.45.59.59,0,0,1,.44-.18.6.6,0,0,1,.44.18.58.58,0,0,1,.19.45.58.58,0,0,1-.19.44A.6.6,0,0,1,289.83,306.37Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M296.33,313.62a3.1,3.1,0,0,1-1.64.41,2.68,2.68,0,0,1-2.07-.83,3,3,0,0,1-.79-2.17,3.32,3.32,0,0,1,.85-2.38,3,3,0,0,1,2.27-.9,3.06,3.06,0,0,1,1.39.3v1a2.45,2.45,0,0,0-1.43-.47,2,2,0,0,0-1.51.66,2.52,2.52,0,0,0-.59,1.73,2.41,2.41,0,0,0,.56,1.67,1.92,1.92,0,0,0,1.48.61,2.4,2.4,0,0,0,1.48-.53Z" fill="#1e1e1e"/>
<path d="M302.08,313.89h-1V313h0a2,2,0,0,1-1.84,1.07,1.93,1.93,0,0,1-1.4-.47,1.65,1.65,0,0,1-.51-1.26c0-1.12.66-1.77,2-2l1.8-.25c0-1-.41-1.53-1.24-1.53a3,3,0,0,0-2,.74v-1a3.68,3.68,0,0,1,2-.57c1.41,0,2.12.75,2.12,2.24Zm-1-3-1.45.2a2.35,2.35,0,0,0-1,.33,1,1,0,0,0-.34.84.93.93,0,0,0,.32.72,1.22,1.22,0,0,0,.83.28,1.52,1.52,0,0,0,1.18-.51,1.77,1.77,0,0,0,.47-1.26Z" fill="#1e1e1e"/>
<path d="M306.67,313.83a1.76,1.76,0,0,1-.89.19c-1,0-1.58-.58-1.58-1.76v-3.55h-1v-.82h1v-1.46l1-.31v1.77h1.51v.82h-1.51v3.38a1.41,1.41,0,0,0,.21.87.81.81,0,0,0,.68.25.94.94,0,0,0,.62-.2Z" fill="#1e1e1e"/>
<path d="M308.45,306.37a.6.6,0,0,1-.44-.18.57.57,0,0,1-.18-.44.61.61,0,0,1,.18-.45.6.6,0,0,1,.44-.18.64.64,0,0,1,.45.18.61.61,0,0,1,.18.45.6.6,0,0,1-.18.44A.64.64,0,0,1,308.45,306.37Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M313.37,314a2.76,2.76,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.26,3.26,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.69,2.69,0,0,1,2.09.83,3.75,3.75,0,0,1-.05,4.59A2.83,2.83,0,0,1,313.37,314Zm.07-5.47a1.8,1.8,0,0,0-1.46.63,2.59,2.59,0,0,0-.54,1.74,2.46,2.46,0,0,0,.54,1.68,1.85,1.85,0,0,0,1.46.62,1.76,1.76,0,0,0,1.43-.61,2.58,2.58,0,0,0,.5-1.72,2.62,2.62,0,0,0-.5-1.73A1.73,1.73,0,0,0,313.44,308.56Z" fill="#1e1e1e"/>
<path d="M322.87,313.89h-1v-3.42c0-1.27-.46-1.91-1.39-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.48,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.82,1.82,0,0,1,1.5.64,2.8,2.8,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M281.47,353.58h-1.09l-.89-2.36h-3.56l-.84,2.36H274l3.23-8.4h1Zm-2.3-3.24-1.32-3.58a3.65,3.65,0,0,1-.13-.56h0a3.49,3.49,0,0,1-.14.56l-1.31,3.58Z" fill="#1e1e1e"/>
<path d="M287.43,353.58h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.14-.85-2.14-2.55v-3.59h.95V351c0,1.27.49,1.9,1.46,1.9a1.45,1.45,0,0,0,1.15-.52,1.94,1.94,0,0,0,.46-1.35v-3.46h1Z" fill="#1e1e1e"/>
<path d="M292.15,353.52a1.8,1.8,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M298.41,353.58h-1v-3.46c0-1.25-.46-1.87-1.39-1.87a1.51,1.51,0,0,0-1.18.54,2,2,0,0,0-.48,1.39v3.4h-1V344.7h1v3.88h0a2.18,2.18,0,0,1,2-1.14c1.35,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
<path d="M302.73,353.72a2.79,2.79,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.23,3.23,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.69,2.69,0,0,1,2.1.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.87,2.87,0,0,1,302.73,353.72Zm.07-5.47a1.83,1.83,0,0,0-1.46.63,2.58,2.58,0,0,0-.54,1.73,2.47,2.47,0,0,0,.54,1.69,1.86,1.86,0,0,0,1.46.61,1.79,1.79,0,0,0,1.44-.6,3.23,3.23,0,0,0,0-3.45A1.76,1.76,0,0,0,302.8,348.25Z" fill="#1e1e1e"/>
<path d="M310.39,348.55a1.18,1.18,0,0,0-.73-.19,1.23,1.23,0,0,0-1,.58,2.64,2.64,0,0,0-.41,1.58v3.06h-1v-6h1v1.24h0a2.07,2.07,0,0,1,.63-1,1.4,1.4,0,0,1,.94-.36,1.5,1.5,0,0,1,.58.09Z" fill="#1e1e1e"/>
<path d="M311.92,346.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.61.61,0,0,1,.45.18.6.6,0,0,1,.18.44.63.63,0,0,1-.63.63Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M318.52,347.85,315,352.76h3.51v.82h-4.92v-.3l3.55-4.88h-3.22v-.82h4.63Z" fill="#1e1e1e"/>
<path d="M324.06,353.58h-1v-.94h0a2.2,2.2,0,0,1-3.25.61,1.65,1.65,0,0,1-.51-1.26c0-1.13.67-1.78,2-2l1.79-.25q0-1.53-1.23-1.53a2.92,2.92,0,0,0-2,.74v-1a3.76,3.76,0,0,1,2-.56c1.41,0,2.12.74,2.12,2.24Zm-1-3-1.44.2a2.59,2.59,0,0,0-1,.33,1,1,0,0,0-.34.85.89.89,0,0,0,.31.71,1.2,1.2,0,0,0,.84.28,1.56,1.56,0,0,0,1.18-.5,1.79,1.79,0,0,0,.46-1.27Z" fill="#1e1e1e"/>
<path d="M328.65,353.52a1.82,1.82,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82h-1.51v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M330.42,346.06a.62.62,0,0,1-.44-.18.61.61,0,0,1-.18-.45A.6.6,0,0,1,330,345a.62.62,0,0,1,.44-.18.61.61,0,0,1,.45.18.58.58,0,0,1,.19.44.56.56,0,0,1-.19.44A.58.58,0,0,1,330.42,346.06Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M335.35,353.72a2.79,2.79,0,0,1-2.13-.84,3.1,3.1,0,0,1-.79-2.23,3.23,3.23,0,0,1,.82-2.36,3,3,0,0,1,2.24-.85,2.68,2.68,0,0,1,2.09.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.85,2.85,0,0,1,335.35,353.72Zm.07-5.47a1.84,1.84,0,0,0-1.47.63,2.58,2.58,0,0,0-.54,1.73,2.42,2.42,0,0,0,.55,1.69,1.85,1.85,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.73,1.73,0,0,0,335.42,348.25Z" fill="#1e1e1e"/>
<path d="M344.85,353.58h-1v-3.42c0-1.28-.47-1.91-1.4-1.91a1.52,1.52,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.17,2.17,0,0,1,2-1.14,1.85,1.85,0,0,1,1.51.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M414.56,353.23a4.89,4.89,0,0,1-2.32.49,3.74,3.74,0,0,1-2.87-1.15,4.26,4.26,0,0,1-1.08-3,4.46,4.46,0,0,1,1.22-3.26,4.08,4.08,0,0,1,3.07-1.24,4.92,4.92,0,0,1,2,.34v1.05a4,4,0,0,0-2-.5,3.08,3.08,0,0,0-2.35,1,3.66,3.66,0,0,0-.9,2.59,3.41,3.41,0,0,0,.85,2.44,2.85,2.85,0,0,0,2.2.92,4.06,4.06,0,0,0,2.19-.57Z" fill="#1e1e1e"/>
<path d="M421,353.58h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.14-.85-2.14-2.55v-3.59h1V351c0,1.27.49,1.9,1.45,1.9a1.46,1.46,0,0,0,1.16-.52A2,2,0,0,0,420,351v-3.46h1Z" fill="#1e1e1e"/>
<path d="M422.57,353.36v-1a2.84,2.84,0,0,0,1.73.58c.85,0,1.27-.28,1.27-.84a.76.76,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,3,3,0,0,0-.44-.23l-.53-.21a6.76,6.76,0,0,1-.7-.32,2.29,2.29,0,0,1-.51-.36,1.52,1.52,0,0,1-.3-.46,1.66,1.66,0,0,1-.11-.61,1.39,1.39,0,0,1,.2-.74,1.78,1.78,0,0,1,.51-.55,2.26,2.26,0,0,1,.74-.33,3.41,3.41,0,0,1,.85-.11,3.46,3.46,0,0,1,1.4.27v1a2.74,2.74,0,0,0-1.53-.43,1.75,1.75,0,0,0-.48.06,1.33,1.33,0,0,0-.38.17.88.88,0,0,0-.24.27.77.77,0,0,0-.08.34.87.87,0,0,0,.08.39.83.83,0,0,0,.25.28,1.91,1.91,0,0,0,.4.23l.53.21c.27.11.51.21.72.32a2.82,2.82,0,0,1,.54.36,1.41,1.41,0,0,1,.34.47,1.46,1.46,0,0,1,.12.62,1.42,1.42,0,0,1-.2.78,1.58,1.58,0,0,1-.52.54,2.26,2.26,0,0,1-.76.32,3.68,3.68,0,0,1-.89.11A3.41,3.41,0,0,1,422.57,353.36Z" fill="#1e1e1e"/>
<path d="M430.81,353.52a1.8,1.8,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82H429.3v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1,1,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M434.6,353.72a2.79,2.79,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.23,3.23,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.69,2.69,0,0,1,2.1.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.87,2.87,0,0,1,434.6,353.72Zm.07-5.47a1.83,1.83,0,0,0-1.46.63,2.58,2.58,0,0,0-.54,1.73,2.47,2.47,0,0,0,.54,1.69,1.86,1.86,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.17,3.17,0,0,0,0-3.45A1.73,1.73,0,0,0,434.67,348.25Z" fill="#1e1e1e"/>
<path d="M447.65,353.58h-1v-3.45a2.66,2.66,0,0,0-.3-1.44,1.19,1.19,0,0,0-1-.44,1.25,1.25,0,0,0-1,.56,2.12,2.12,0,0,0-.43,1.35v3.42h-1V350c0-1.18-.45-1.77-1.36-1.77a1.24,1.24,0,0,0-1,.53,2.14,2.14,0,0,0-.41,1.38v3.42h-1v-6h1v.95h0a2,2,0,0,1,1.86-1.09,1.76,1.76,0,0,1,1.08.34,1.73,1.73,0,0,1,.62.9,2.14,2.14,0,0,1,2-1.24c1.32,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
<path d="M452.73,353.58h-1v-7.26a1.74,1.74,0,0,1-.33.25,5.06,5.06,0,0,1-.48.29,5.85,5.85,0,0,1-.56.26,5,5,0,0,1-.58.2v-1a5.46,5.46,0,0,0,.68-.23c.23-.1.46-.22.69-.34a5.88,5.88,0,0,0,.65-.39,4.87,4.87,0,0,0,.53-.39h.36Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M517.25,353.23a4.89,4.89,0,0,1-2.32.49,3.74,3.74,0,0,1-2.87-1.15,4.26,4.26,0,0,1-1.08-3,4.5,4.5,0,0,1,1.21-3.26,4.12,4.12,0,0,1,3.08-1.24,4.92,4.92,0,0,1,2,.34v1.05a4,4,0,0,0-2-.5,3.07,3.07,0,0,0-2.35,1,3.66,3.66,0,0,0-.9,2.59,3.44,3.44,0,0,0,.84,2.44,2.87,2.87,0,0,0,2.21.92,4.11,4.11,0,0,0,2.19-.57Z" fill="#1e1e1e"/>
<path d="M523.69,353.58h-1v-.95h0a2,2,0,0,1-1.85,1.09c-1.43,0-2.15-.85-2.15-2.55v-3.59h1V351c0,1.27.48,1.9,1.45,1.9a1.46,1.46,0,0,0,1.16-.52,2,2,0,0,0,.45-1.35v-3.46h1Z" fill="#1e1e1e"/>
<path d="M525.26,353.36v-1a2.82,2.82,0,0,0,1.73.58c.84,0,1.27-.28,1.27-.84a.76.76,0,0,0-.11-.41,1.22,1.22,0,0,0-.3-.3,2.44,2.44,0,0,0-.43-.23l-.54-.21c-.26-.11-.49-.21-.7-.32a2.22,2.22,0,0,1-.5-.36,1.35,1.35,0,0,1-.3-.46,1.66,1.66,0,0,1-.11-.61,1.39,1.39,0,0,1,.2-.74,1.78,1.78,0,0,1,.51-.55,2.26,2.26,0,0,1,.74-.33,3.56,3.56,0,0,1,2.24.16v1a2.68,2.68,0,0,0-1.52-.43,1.88,1.88,0,0,0-.49.06,1.4,1.4,0,0,0-.37.17.88.88,0,0,0-.24.27.77.77,0,0,0-.08.34.87.87,0,0,0,.08.39.83.83,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21c.27.11.51.21.72.32a2.82,2.82,0,0,1,.54.36,1.41,1.41,0,0,1,.34.47,1.46,1.46,0,0,1,.12.62,1.51,1.51,0,0,1-.2.78,1.58,1.58,0,0,1-.52.54,2.26,2.26,0,0,1-.76.32,3.68,3.68,0,0,1-.89.11A3.39,3.39,0,0,1,525.26,353.36Z" fill="#1e1e1e"/>
<path d="M533.5,353.52a1.82,1.82,0,0,1-.9.19c-1.05,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82H532v3.38a1.45,1.45,0,0,0,.2.86.84.84,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M537.29,353.72a2.77,2.77,0,0,1-2.12-.84,3.11,3.11,0,0,1-.8-2.23,3.23,3.23,0,0,1,.83-2.36,3,3,0,0,1,2.23-.85,2.71,2.71,0,0,1,2.1.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.87,2.87,0,0,1,537.29,353.72Zm.07-5.47a1.81,1.81,0,0,0-1.46.63,2.58,2.58,0,0,0-.54,1.73,2.47,2.47,0,0,0,.54,1.69,1.86,1.86,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.73,1.73,0,0,0,537.36,348.25Z" fill="#1e1e1e"/>
<path d="M550.33,353.58h-1v-3.45a2.57,2.57,0,0,0-.31-1.44,1.16,1.16,0,0,0-1-.44,1.28,1.28,0,0,0-1,.56,2.18,2.18,0,0,0-.43,1.35v3.42h-1V350c0-1.18-.45-1.77-1.36-1.77a1.24,1.24,0,0,0-1,.53,2.14,2.14,0,0,0-.42,1.38v3.42h-1v-6h1v.95h0a2,2,0,0,1,1.86-1.09,1.73,1.73,0,0,1,1.7,1.24,2.14,2.14,0,0,1,2-1.24c1.32,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
<path d="M555.77,353.71a.63.63,0,0,1-.46-.2.6.6,0,0,1-.19-.46.62.62,0,0,1,.19-.46.63.63,0,0,1,.46-.2.65.65,0,0,1,.47.2.62.62,0,0,1,.19.46.6.6,0,0,1-.19.46A.65.65,0,0,1,555.77,353.71Z" fill="#1e1e1e"/>
<path d="M558.37,353.71a.63.63,0,0,1-.46-.2.64.64,0,0,1-.19-.46.66.66,0,0,1,.19-.46.64.64,0,0,1,.93,0,.62.62,0,0,1,.19.46.6.6,0,0,1-.19.46A.63.63,0,0,1,558.37,353.71Z" fill="#1e1e1e"/>
<path d="M561,353.71a.63.63,0,0,1-.46-.2.64.64,0,0,1-.19-.46.66.66,0,0,1,.19-.46.67.67,0,0,1,1.13.46.61.61,0,0,1-.2.46A.63.63,0,0,1,561,353.71Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M434.72,307.23a4.92,4.92,0,0,1-2.32.49,3.72,3.72,0,0,1-2.87-1.15,4.26,4.26,0,0,1-1.08-3,4.45,4.45,0,0,1,1.21-3.26,4.11,4.11,0,0,1,3.08-1.24,5,5,0,0,1,2,.34v1.05a4,4,0,0,0-2-.5,3.07,3.07,0,0,0-2.35,1,3.66,3.66,0,0,0-.9,2.59,3.44,3.44,0,0,0,.84,2.44,2.87,2.87,0,0,0,2.21.92,4.11,4.11,0,0,0,2.19-.57Z" fill="#1e1e1e"/>
<path d="M441.15,307.58h-1v-.95h0a2,2,0,0,1-1.86,1.09c-1.42,0-2.14-.85-2.14-2.55v-3.59h1V305c0,1.27.48,1.9,1.45,1.9a1.47,1.47,0,0,0,1.16-.52,2,2,0,0,0,.45-1.35v-3.46h1Z" fill="#1e1e1e"/>
<path d="M442.73,307.36v-1a2.82,2.82,0,0,0,1.73.58q1.26,0,1.26-.84a.68.68,0,0,0-.11-.41,1.17,1.17,0,0,0-.29-.3,2.86,2.86,0,0,0-.43-.23l-.54-.21a6.76,6.76,0,0,1-.7-.32,2.22,2.22,0,0,1-.5-.36,1.38,1.38,0,0,1-.31-.46,1.66,1.66,0,0,1-.1-.61,1.38,1.38,0,0,1,.19-.74,1.7,1.7,0,0,1,.52-.55,2.21,2.21,0,0,1,.73-.33,3.49,3.49,0,0,1,.86-.11,3.41,3.41,0,0,1,1.39.27v1a2.7,2.7,0,0,0-1.52-.43,1.83,1.83,0,0,0-.49.06,1.26,1.26,0,0,0-.37.17.68.68,0,0,0-.24.27.67.67,0,0,0-.09.34.75.75,0,0,0,.09.39.74.74,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21c.26.11.5.21.71.32a2.82,2.82,0,0,1,.54.36,1.37,1.37,0,0,1,.47,1.09,1.51,1.51,0,0,1-.2.78,1.61,1.61,0,0,1-.53.54,2.21,2.21,0,0,1-.75.32,3.76,3.76,0,0,1-.9.11A3.37,3.37,0,0,1,442.73,307.36Z" fill="#1e1e1e"/>
<path d="M451,307.52a1.82,1.82,0,0,1-.9.19c-1.05,0-1.58-.59-1.58-1.76V302.4h-1v-.82h1v-1.47l1-.31v1.78H451v.82h-1.52v3.38a1.37,1.37,0,0,0,.21.86.83.83,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
<path d="M454.76,307.72a2.79,2.79,0,0,1-2.13-.84,3.1,3.1,0,0,1-.79-2.23,3.23,3.23,0,0,1,.82-2.36,3,3,0,0,1,2.24-.85,2.68,2.68,0,0,1,2.09.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.85,2.85,0,0,1,454.76,307.72Zm.07-5.47a1.84,1.84,0,0,0-1.47.63,2.58,2.58,0,0,0-.54,1.73,2.42,2.42,0,0,0,.55,1.69,1.85,1.85,0,0,0,1.46.61,1.75,1.75,0,0,0,1.43-.6,3.23,3.23,0,0,0,0-3.45A1.73,1.73,0,0,0,454.83,302.25Z" fill="#1e1e1e"/>
<path d="M467.8,307.58h-1v-3.45a2.57,2.57,0,0,0-.31-1.44,1.17,1.17,0,0,0-1-.44,1.28,1.28,0,0,0-1.05.56,2.12,2.12,0,0,0-.43,1.35v3.42h-1V304c0-1.18-.46-1.77-1.37-1.77a1.24,1.24,0,0,0-1,.53,2.2,2.2,0,0,0-.41,1.38v3.42h-1v-6h1v.95h0a2,2,0,0,1,1.87-1.09,1.71,1.71,0,0,1,1.07.34,1.81,1.81,0,0,1,.63.9,2.13,2.13,0,0,1,2-1.24c1.32,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
<path d="M481.42,307.58h-1v-3.45a2.57,2.57,0,0,0-.31-1.44,1.17,1.17,0,0,0-1-.44,1.28,1.28,0,0,0-1.05.56,2.18,2.18,0,0,0-.43,1.35v3.42h-1V304c0-1.18-.45-1.77-1.36-1.77a1.27,1.27,0,0,0-1.05.53,2.2,2.2,0,0,0-.41,1.38v3.42h-1v-6h1v.95h0a2,2,0,0,1,1.86-1.09,1.73,1.73,0,0,1,1.7,1.24,2.14,2.14,0,0,1,2-1.24c1.32,0,2,.81,2,2.44Z" fill="#1e1e1e"/>
<path d="M483.73,300.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.61.61,0,0,1,.45.18.6.6,0,0,1,.18.44.63.63,0,0,1-.63.63Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M491.27,307.58h-1v-1h0a2.21,2.21,0,0,1-2.06,1.16,2.24,2.24,0,0,1-1.81-.81,3.28,3.28,0,0,1-.68-2.19,3.57,3.57,0,0,1,.75-2.38,2.48,2.48,0,0,1,2-.9,1.91,1.91,0,0,1,1.8,1h0V298.7h1Zm-1-2.71V304a1.7,1.7,0,0,0-.49-1.23,1.61,1.61,0,0,0-1.21-.5,1.67,1.67,0,0,0-1.39.64,2.87,2.87,0,0,0-.5,1.78,2.56,2.56,0,0,0,.48,1.64,1.59,1.59,0,0,0,1.3.6,1.61,1.61,0,0,0,1.3-.58A2.13,2.13,0,0,0,490.31,304.87Z" fill="#1e1e1e"/>
<path d="M498.33,307.58h-1v-1h0a2.42,2.42,0,0,1-3.87.35,3.28,3.28,0,0,1-.68-2.19,3.57,3.57,0,0,1,.75-2.38,2.48,2.48,0,0,1,2-.9,1.93,1.93,0,0,1,1.8,1h0V298.7h1Zm-1-2.71V304a1.73,1.73,0,0,0-.48-1.23,1.64,1.64,0,0,0-1.22-.5,1.66,1.66,0,0,0-1.38.64,2.81,2.81,0,0,0-.5,1.78,2.56,2.56,0,0,0,.48,1.64,1.57,1.57,0,0,0,1.3.6,1.63,1.63,0,0,0,1.3-.58A2.17,2.17,0,0,0,497.37,304.87Z" fill="#1e1e1e"/>
<path d="M501.24,307.58h-1V298.7h1Z" fill="#1e1e1e"/>
<path d="M508,304.82h-4.24a2.3,2.3,0,0,0,.54,1.55,1.87,1.87,0,0,0,1.42.54,2.93,2.93,0,0,0,1.86-.67v.91a3.5,3.5,0,0,1-2.09.57,2.52,2.52,0,0,1-2-.82,3.36,3.36,0,0,1-.73-2.3,3.29,3.29,0,0,1,.8-2.28,2.53,2.53,0,0,1,2-.88,2.25,2.25,0,0,1,1.82.76,3.18,3.18,0,0,1,.65,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.36,1.36,0,0,0-1.1-.46,1.55,1.55,0,0,0-1.15.48,2.25,2.25,0,0,0-.59,1.28Z" fill="#1e1e1e"/>
<path d="M517,301.58l-1.8,6h-1l-1.23-4.3a2.84,2.84,0,0,1-.1-.55h0a2.44,2.44,0,0,1-.12.54l-1.35,4.31h-1l-1.81-6h1l1.24,4.51a3.12,3.12,0,0,1,.08.54H511a2.29,2.29,0,0,1,.1-.55l1.38-4.5h.88l1.25,4.52a5.29,5.29,0,0,1,.08.54h0a2.61,2.61,0,0,1,.1-.54l1.22-4.52Z" fill="#1e1e1e"/>
<path d="M522.43,307.58h-1v-.94h0a2.2,2.2,0,0,1-3.25.61,1.64,1.64,0,0,1-.5-1.26c0-1.13.66-1.78,2-2l1.8-.25c0-1-.42-1.53-1.24-1.53a2.92,2.92,0,0,0-2,.74v-1a3.76,3.76,0,0,1,2-.56c1.41,0,2.12.74,2.12,2.24Zm-1-3-1.45.2a2.59,2.59,0,0,0-1,.33,1,1,0,0,0-.34.85.89.89,0,0,0,.31.71,1.2,1.2,0,0,0,.84.28,1.55,1.55,0,0,0,1.18-.5,1.8,1.8,0,0,0,.47-1.27Z" fill="#1e1e1e"/>
<path d="M527.37,302.55a1.18,1.18,0,0,0-.73-.19,1.23,1.23,0,0,0-1,.58,2.72,2.72,0,0,0-.41,1.58v3.06h-1v-6h1v1.24h0a2.07,2.07,0,0,1,.63-1,1.4,1.4,0,0,1,.94-.36,1.5,1.5,0,0,1,.58.09Z" fill="#1e1e1e"/>
<path d="M533.23,304.82H529a2.3,2.3,0,0,0,.54,1.55,1.88,1.88,0,0,0,1.42.54,2.89,2.89,0,0,0,1.86-.67v.91a3.47,3.47,0,0,1-2.09.57,2.51,2.51,0,0,1-2-.82,3.83,3.83,0,0,1,.06-4.58,2.54,2.54,0,0,1,2-.88,2.26,2.26,0,0,1,1.83.76,3.18,3.18,0,0,1,.64,2.12Zm-1-.81a2,2,0,0,0-.4-1.3,1.37,1.37,0,0,0-1.1-.46,1.57,1.57,0,0,0-1.16.48A2.24,2.24,0,0,0,529,304Z" fill="#1e1e1e"/>
<path d="M534.32,307.36v-1a2.82,2.82,0,0,0,1.73.58q1.26,0,1.26-.84a.76.76,0,0,0-.1-.41,1.22,1.22,0,0,0-.3-.3,2.44,2.44,0,0,0-.43-.23l-.54-.21c-.26-.11-.5-.21-.7-.32a2.22,2.22,0,0,1-.5-.36,1.22,1.22,0,0,1-.3-.46,1.66,1.66,0,0,1-.11-.61,1.39,1.39,0,0,1,.2-.74,1.67,1.67,0,0,1,.51-.55,2.26,2.26,0,0,1,.74-.33,3.56,3.56,0,0,1,2.24.16v1a2.7,2.7,0,0,0-1.52-.43,1.88,1.88,0,0,0-.49.06,1.4,1.4,0,0,0-.37.17.88.88,0,0,0-.24.27.77.77,0,0,0-.08.34.87.87,0,0,0,.08.39.83.83,0,0,0,.25.28,1.68,1.68,0,0,0,.4.23l.53.21c.27.11.51.21.72.32a2.82,2.82,0,0,1,.54.36,1.57,1.57,0,0,1,.34.47,1.46,1.46,0,0,1,.12.62,1.51,1.51,0,0,1-.2.78,1.58,1.58,0,0,1-.52.54,2.26,2.26,0,0,1-.76.32,3.68,3.68,0,0,1-.89.11A3.39,3.39,0,0,1,534.32,307.36Z" fill="#1e1e1e"/>
</g>
<g>
<path d="M638,353.58h-4.46v-8.4h4.27v.89h-3.28v2.79h3v.89h-3v2.94H638Z" fill="#1e1e1e"/>
<path d="M644.48,353.58h-1v-3.42c0-1.28-.46-1.91-1.39-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.47,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.83,1.83,0,0,1,1.5.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
<path d="M651.42,353.58h-1v-1h0a2.21,2.21,0,0,1-2.06,1.16,2.24,2.24,0,0,1-1.81-.81,3.28,3.28,0,0,1-.67-2.19,3.57,3.57,0,0,1,.75-2.38,2.46,2.46,0,0,1,2-.9,1.91,1.91,0,0,1,1.8,1h0V344.7h1Zm-1-2.71V350a1.73,1.73,0,0,0-.48-1.23,1.65,1.65,0,0,0-1.22-.5,1.67,1.67,0,0,0-1.39.64,2.87,2.87,0,0,0-.5,1.78,2.56,2.56,0,0,0,.48,1.64,1.73,1.73,0,0,0,2.6,0A2.13,2.13,0,0,0,650.46,350.87Z" fill="#1e1e1e"/>
<path d="M654.35,352.71h0v3.63h-1v-8.76h1v1.05h0a2.27,2.27,0,0,1,2.07-1.19,2.21,2.21,0,0,1,1.81.8,3.37,3.37,0,0,1,.65,2.16,3.69,3.69,0,0,1-.73,2.41,2.43,2.43,0,0,1-2,.91A2,2,0,0,1,654.35,352.71Zm0-2.42v.84a1.73,1.73,0,0,0,.49,1.26,1.71,1.71,0,0,0,2.59-.15,3,3,0,0,0,.5-1.85,2.39,2.39,0,0,0-.47-1.57,1.51,1.51,0,0,0-1.25-.57,1.7,1.7,0,0,0-1.35.58A2.15,2.15,0,0,0,654.32,350.29Z" fill="#1e1e1e"/>
<path d="M662.92,353.72a2.79,2.79,0,0,1-2.12-.84,3.1,3.1,0,0,1-.79-2.23,3.23,3.23,0,0,1,.82-2.36,3,3,0,0,1,2.23-.85,2.69,2.69,0,0,1,2.1.82,3.32,3.32,0,0,1,.75,2.3,3.25,3.25,0,0,1-.81,2.3A2.87,2.87,0,0,1,662.92,353.72Zm.07-5.47a1.83,1.83,0,0,0-1.46.63,2.58,2.58,0,0,0-.54,1.73,2.42,2.42,0,0,0,.55,1.69,1.84,1.84,0,0,0,1.45.61,1.77,1.77,0,0,0,1.44-.6,3.23,3.23,0,0,0,0-3.45A1.75,1.75,0,0,0,663,348.25Z" fill="#1e1e1e"/>
<path d="M667.94,346.06a.6.6,0,0,1-.44-.18.61.61,0,0,1-.18-.45.61.61,0,0,1,.62-.62.61.61,0,0,1,.45.18.6.6,0,0,1,.18.44.63.63,0,0,1-.63.63Zm.47,7.52h-1v-6h1Z" fill="#1e1e1e"/>
<path d="M675.33,353.58h-1v-3.42c0-1.28-.46-1.91-1.39-1.91a1.5,1.5,0,0,0-1.19.54,2,2,0,0,0-.48,1.37v3.42h-1v-6h1v1h0a2.16,2.16,0,0,1,2-1.14,1.83,1.83,0,0,1,1.5.63,2.84,2.84,0,0,1,.52,1.84Z" fill="#1e1e1e"/>
<path d="M679.93,353.52a1.82,1.82,0,0,1-.9.19c-1,0-1.57-.59-1.57-1.76V348.4h-1v-.82h1v-1.47l1-.31v1.78h1.51v.82h-1.51v3.38a1.38,1.38,0,0,0,.2.86.83.83,0,0,0,.68.26,1.05,1.05,0,0,0,.63-.2Z" fill="#1e1e1e"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -0,0 +1,224 @@
---
title: ASP.NET Core中的静态Web资产
date: 2026-01-04T16:36:36.5629759+08:00
tags:
- 技术笔记
- dotnet
- ASP.NET Core
---
Web服务器应该如何扫描与提供静态Web文件尤其是在考虑到缓存、压缩的情况下还需要正确的处理开发环境和部署环境之间的差异让我们来看看ASP.NET Core是如何处理这个问题的。以及如何将通过其他工具例如`pnpm`生成的前端资产文件集成到ASP.NET Core中。
<!--more-->
### 引言——Blazor开发中的静态Web资源
Blazor是ASP.NET Core中~~新推出的~~Web应用程序开发框架通过一系列精巧的设计实现了使用HTML和C#编写运行在浏览器中的应用程序避免了使用丑陋的JavaScript。但是现代的前端开发生态几乎都建立在JavaScript之上尤其是考虑在JavaScript在很长的一段时间都是浏览器唯一支持的脚本语言在Blazor项目开发的过程中必然会遇到一些只能编写JavaScript才能解决的问题。同时一系列的现代前端工具例如[tailwindcss](https://tailwindcss.com/)提供了更加优秀的前端开发体验但是这些都基于NodeJS和NPM等前端工具。以上的前端生态引入了一个问题如何在MSBuild驱动的Blazor应用构建流程中自然地运行前端工具链和ASP.NET Core支持的服务器部署生成的静态资源
Blazor目前提供了一个入口简洁但是功能丰富的静态Web资源提供功能。在使用默认应用目录的情况下项目将会提供一个`wwwroot`文件夹,这个文件夹中的内容将可以从`/`直接寻址。为了提升前端静态文件的使用体验,该文件夹下的文件将会经过一个复杂的管道:
- 在构建扫描到这些资产文件之后MSBuild将会给静态文件加上内容指纹以防止重复使用旧文件。资源还会被压缩以减少资产交付的时间。
- 在运行时,所发现的资产文件将会作为终结点公开,并添加上合适的缓存头和内容类型头。在设置`ETtag``Last-Modified``Content-Type`头之后,浏览器将可以合理的缓存这些静态文件直到应用更新。
该静态文件功能还需要适应应用程序的部署状态:
- 在开发时,或者说运行`dotnet run``dotnet build`该功能需要将对应的静态文件终结点URL映射到磁盘上存储的实际静态文件上就像它们实际上就在`wwwroot`文件夹中一样。考虑到实际上开发过程中会用到Blazor内部的资产文件`blazor.web.js`,引用项目中的资产文件等等,这实际上一个相当复杂的检测-映射流程。
- 在发布时,或者说运行`dotnet publish`时,该功能需要收集所有需要的静态文件并复制到最终发布文件夹的`wwwroot`文件夹之下。
### Microsoft.AspNetCore.ClientAssets
在默认的应用模板下如果需要使用其他的现代前端工具生成静态资产文件最简单的方法就是手动或者编写MSBuild目标Target生成资产文件并放在`wwwroot`文件夹中。但是这个方法存在着如下几个问题:
- 开发者需要编写 MSBuild 目标targets来调用他们的工具。
- 开发者通常没有在构建过程的恰当时机执行其自定义目标。
- 开发者将工具生成的输出文件放入应用的 wwwroot 文件夹中,这会导致这些文件在下一次构建时被误认为是输入文件。
- 开发者没有为这些工具正确声明输入和输出依赖,导致即使输出文件已是最新,工具仍会重复运行,从而增加构建耗时。
面对这些问题M$提供了一个Alpha状态的库`Microsoft.AspNetCore.ClientAssets`来解决这个问题。不幸的是,这个库已经因为年久失修(上一次[更新](https://github.com/aspnet/AspLabs/pull/572)是在3年前引入对于.NET 7的支持在.NET 9引入新的静态资产部署管线之后使用会直接报错了。
^^ 相关的Issues链接[#38445](https://github.com/dotnet/aspnetcore/issues/38445)[#62925](https://github.com/dotnet/aspnetcore/issues/62925)
为了良好地解决如上的问题我们需要首先了解一下ASP.NET Core中静态资产文件的构建和部署过程。
### StaticWebAssetsSdk
在.NET中构建静态资产文件的相关代码在[dotnet/sdk](https://github.com/dotnet/sdk)仓库中,称作`StaticWebAssetsSdk`
静态 Web 资源会接管应用程序 wwwroot 文件夹中的内容项,并全面管理这些内容。在开发过程中,系统会生成一个 JSON 清单manifest其中包含以下信息
- 版本号version number标识清单格式的版本。
- 清单内容的哈希值hash用于判断清单内容是否发生变化。
- 库的包 IDlibrary package id用于区分当前项目与其他项目所提供的资源。
- 库的资源基础路径asset base path在将其他库的路径应用“发现模式”时用于确定要添加的基础路径。
- 清单模式manifest mode定义来自特定项目的资源在构建和发布时应如何处理。
- 相关项目清单及其哈希值的列表:用于判断自清单生成以来,项目引用是否发生变化,或是否有清单被更新。
- “发现模式”discovery patterns列表用于在清单构建完成后有选择性地在运行时提供某些资源。例如可以使用如下模式
```json
{ "Path": "<Project>/Pages", "BasePath": "_content/Something", "Pattern": "**/*.js" }
```
表示仅提供该目录下扩展名为`js`的文件。如果有人添加了图片或其他文件,它们将不会被提供。(这一点很重要,因为这些文件并不符合任何资源规则,也不会包含在发布输出目录中。)
- 构建/发布过程中生成的静态 Web 资源列表。
系统会生成两套清单:**构建清单build manifest** 和 **发布清单publish manifest**
- **构建清单**在构建过程中生成,用于开发阶段,使资源表现得如同它们属于应用程序本身。
- **发布清单**在发布过程中生成,记录了发布阶段对资源执行的所有转换操作。
资源可以在构建阶段或发布阶段定义,并可在任意阶段被标记为“仅构建”或“仅发布”。例如,你可以有两个文件:一个用于开发,一个用于发布,但它们都需要通过相同的 URL 路径提供服务。`service-worker.js` 就是一个典型例子。
**构建时清单**由项目中发现的资源以及来自被引用项目和包的资源共同组成。
**发布清单**则以构建清单为基础,过滤掉仅用于构建的文件,并包含在发布过程中对这些文件执行的所有转换(如链接、打包、压缩等)。
这种机制使得在发布阶段可以执行如链接Linking、打包Bundling、压缩Compression等优化操作。被引用的项目也会生成自己的发布清单其内容会在发布过程中与当前项目的清单合并。同时在发布过程中我们仍会保留被引用项目的原始构建清单以便应用程序可以选择忽略被引用项目的发布资源并对整个依赖传递闭包中的资源执行全局优化。例如一个类库在发布时可能生成一个压缩后的 JS 包,而主应用可以选择不使用多个独立的包,而是收集所有原始构建阶段的资源,生成一个统一的包。通常情况下,构建清单和发布清单内容相同,除非存在仅在发布阶段才应用的转换。
每份清单中会列出在构建/发布过程中生成或计算出的所有资源及其属性。这些属性包括:
- **Identity**:资源的唯一标识(文件的完整路径)。
- **SourceType**:资源类型('Discovered', 'Computed', 'Project', 'Package')。
- **ContentRoot**:开发阶段资源暴露的原始路径。
- **BasePath**:资源暴露的基础路径。
- **RelativePath**:资源的相对路径。
- **AssetKind**:资源用途('Build', 'Publish', 'All'),由 `CopyToOutputDirectory` / `CopyToPublishDirectory` 推断得出。
- **AssetMode**:资源作用范围('CurrentProject', 'Reference', 'All')。
- **AssetRole**:资源角色('Primary', 'Related', 'Alternative')。
- **AssetMergeSource**:当资源被嵌入到其他 TFM目标框架时的来源。
- **AssetMergeBehavior**:当同一 TFM 中出现资源冲突时的合并行为。
- **RelatedAsset**:当前资源所依赖的主资源的 Identity。
- **AssetTraitName**:区分相关或替代资源与主资源的特征名称(如语言、编码格式等)。
- **AssetTraitValue**:该特征的具体值。
- **CopyToBuildDirectory**:与 Content 项一致(如 PreserveNewest、Always
- **CopyToPublishDirectory**:与 Content 项一致。
- **OriginalItemSpec**:定义该资源的原始项规范。
关于资源在不同场景下的使用(作为主项目的一部分,或作为被引用项目的一部分),有三种可能的选项:
- **All**:资源在所有情况下都应被使用。
- **Root**:资源仅在当前项目作为主项目构建时使用。
- **Reference**:资源仅在当前项目被其他项目引用时使用。
例如CSS 隔离CSS isolation生成的两个包
- `<<Project>>.styles.css` 是 **Root** 资源,仅在作为主项目时使用。
- `<<Project>>.lib.bundle.css` 是 **Reference** 资源,仅在被其他项目引用时使用。
除了上述三种使用模式,项目还需定义其在构建和发布过程中如何处理清单中的文件。对此有三种模式:
- **Default**:项目在发布时将所有内容复制到发布输出目录,但当被其他项目引用时不做任何操作,而是期望引用方负责处理静态 Web 资源的发布。
→ 通常用于类库class libraries
- **Root**:项目被视为静态 Web 资源的“根”,即使被引用,其资源也应像主项目一样被处理(例如,不复制传递依赖资源,而只复制 Root 资源)。
→ 用于如 Blazor WebAssembly 托管项目(被 ASP.NET Core 主机项目引用,但资源应视为根项目)。
- **Isolated**:与 Root 类似,但引用项目完全不知道静态 Web 资源的存在;当前项目会自行在发布时设置处理程序,将资源复制到正确位置。
→ 用于如 Blazor 桌面应用,将静态 Web 资源自动纳入 `GetCopyToPublishDirectoryItems`,使引用方无需了解静态 Web 资源机制。
关于资源类型,静态 Web 资源可分为四类:
- **Discovered assets**:从项目中已有项(如 Content、None 等)中发现的资源。
- **Computed assets**:在构建过程中生成、需要在构建时复制到最终位置的资源。
- **Project**:来自被引用项目的资源。当合并被引用项目的清单时,其 Discovered 和 Computed 资源会转换为 Project 类型。
- **Package**:来自被引用 NuGet 包的资源。
关于资源角色Asset Role有三种
- **Primary主资源**:表示可通过其相对路径直接访问的资源。大多数资源属于此类。
- **Related相关资源**:表示与另一个资源相关,但两者都可通过各自的相对路径独立访问。
- **Alternative替代资源**:表示是另一个资源的替代形式,例如预压缩版本或不同格式版本。通常应通过与主资源相同的相对路径提供(具体实现由运行时决定)。静态 Web 资源层仅记录这种关系。
对于 Related 和 Alternative 资源,其 `RelatedAsset` 属性指向其所依赖的主资源。这种依赖链可多层嵌套,以表示一个资源的多种表示形式。静态 Web 资源仅记录这些信息,具体如何使用由 MSBuild 目标决定。
`AssetTraitName` 和 `AssetTraitValue` 用于区分相关/替代资源与其主资源。例如:
- 对于全球化程序集可记录程序集的文化culture
- 对于压缩资源,可记录编码方式(如 gzip、brotli
下图展示了在构建过程中被调用的MSBuild Target
![image-20251231225433184](./aspnetcore-swa/image-20251231225433184.webp)
Sdk提供了一些重要的MSBuild Task供程序员调用
- `DefineStaticWebAssets`该Task扫描提供了一系列候选的资产文件并构建一个*标准化的*静态资产对象;
- `DefineStaticWebAssetEndpoints`该Task以上一个任务输出的静态资产对象为输入输出每个静态资产文件的Web终结点
在构建中过程中`GenerateStaticWebAssetsManifest`和`GenerateStaticWebAssetsDevelopmentManifest`、`GenerateStaticWebAssetEndpointsManifest`等几个任务会产生一个重要的清单文件,这些文件通常存放在*obj*文件夹中,名称为`staticwebassets.*.json`。其中一个较为重要的清单文件是`staticwebassets.development.json`,其存储了所有的静态资产文件和对应的存储目录。这个文件在构建的过程中会被复制到输出目录`bin`中,名称为`$(PackageId).staticwebassets.runtime.json`。这个文件将会在生产模式下被静态资产中间件读取,作为建立静态文件终结点到实际物理文件的索引。这个文件也为需要调试`StaticWebAssetsSdk`的程序员提供了重要的调试信息是解决ASP.NET Core中静态资产问题的不二法门。
### 解决方案
现在已经充分了解了`StaticWebAssetsSdk`可以来设计在MSBuild中集成前端工具并生成最终静态资产文件的管线了。
首先来研究过程的步骤,`npm`或者其类似物也使用类似于MSBuild的先还原再构建两步首先需要安装程序中使用到的包然后在运行构建指令构建对应的静态文件构建完成之后还需要将构建产物交给MSBuild中的静态资产处理管线进行进一步的处理。因此设计如下的三个步骤
1. `RestoreClientAssets`这个Target需要运行`npm install`或者类似的指令安装依赖包;
2. `BuildClientAssets`这个Target运行`npm run build`或者类似的指令构建项目;
3. `DefineClientAssets`这个Target调用`DefineStaticWebAssets`等Task声明静态资产文件。
确定好生成步骤之后,声明一下会在生成过程中会用到的,可以提供给用户自定义的属性。安装和构建的相应软件包肯定是需要提供给用户自定义的。在一般情况下,前端工具链把将静态文件生成到`dist`文件夹中。为了符合MSBuild的惯例这里将中间静态文件生成到*obj*文件夹下的`ClientAssets`文件夹中。为了实现这一点,构建过程中的指令就需要支持一个指定生成目录的参数,这个参数也作为一个属性暴露给用户可以自定义。这里就形成了下面三个提供给用户自定义的参数。
```xml
<PropertyGroup>
<ClientAssetsRestoreCommand Condition="'$(ClientAssesRestoreCommand)' == ''">pnpm install</ClientAssetsRestoreCommand>
<ClientAssetsBuildCommand Condition="'$(ClientAssetsBuildCommand)' == ''">pnpm run build</ClientAssetsBuildCommand>
<ClientAssetsBuildOutputParameter Condition="'$(ClientAssetsBuildOutputParameter)' == ''">--output</ClientAssetsBuildOutputParameter>
</PropertyGroup>
```
最终就是完成的构建原始代码了。第一个运行的构建目标`RestoreClientAssets`将会在`DispatchToInnerBuilds`任务运行之后运行这个目标是MSBuild构建管线中一个不论是针对单架构生成还是多架构生成都只会运行一次的目标这样在项目需要同时编译到.NET 8和.NET 10的情况下仍然只会运行前端的安装命令一次。`BuildClientAssets`目标紧接着`RestoreClientAssets`目标的运行而运行,并将所有生成的前端文件添加到`_ClientAssetsBuildOutput`项中。最终的`DefineClientAssets`目标在负责解析项目中的所有静态文件的目标`ResolveWebAssetsConfiguration`运行之前运行,调用`DefineStaticWebAssets`和`DefineStaticWebAssetEndpoints`将前面生成的所有前端静态文件添加到MSBuild的静态文件处理管线中。
```xml
<PropertyGroup>
<_RestoreClientAssetsBeforeTargets Condition="'$(TargetFramework)' == ''">DispatchToInnerBuilds</_RestoreClientAssetsBeforeTargets>
</PropertyGroup>
<Target Name="RestoreClientAssets" BeforeTargets="$(_RestoreClientAssetsBeforeTargets)">
<Message Importance="high" Text="Running $(ClientAssetsRestoreCommand)"/>
<Exec Command="$(ClientAssetsRestoreCommand)"/>
</Target>
<Target Name="BuildClientAssets" DependsOnTargets="RestoreClientAssets" BeforeTargets="AssignTargetPaths">
<PropertyGroup>
<_ClientAssetsOutputFullPath>$([System.IO.Path]::GetFullPath('$(IntermediateOutputPath)ClientAssets'))</_ClientAssetsOutputFullPath>
</PropertyGroup>
<MakeDir Directories="$(_ClientAssetsOutputFullPath"/>
<Exec Command="$(ClientAssetsBuildCommand) -- $(ClientAssetsBuildOutputParameter) $(_ClientAssetsOutputFullPath)"/>
<ItemGroup>
<_ClientAssetsBuildOutput Include="$(IntermediateOutputPath)ClientAssets\**"/>
</ItemGroup>
</Target>
<Target Name="DefineClientAssets" AfterTargets="BuildClientAssets" DependsOnTargets="ResolveWebAssetsConfiguration">
<ItemGroup>
<FileWrites Include="@(_ClientAssetsBuildOutput)"/>
</ItemGroup>
<DefineStaticWebAssets
CandidateAssets="@(_ClientAssetsBuildOutput)"
SourceId="$(PackageId)"
SourceType="Computed"
ContentRoot="$(_ClientAssetsOutputFullPath)"
BasePath="$(StaticWebAssetBasePath)"
>
<Output TaskParameter="Assets" ItemName="StaticWebAsset"/>
<Output TaskParameter="Assets" ItemName="_ClientAssetsStaticWebAsset"/>
</DefineStaticWebAssets>
<DefineStaticWebAssetEndpoints
CandidateAssets="@(_ClientAssetsStaticWebAsset)"
ContentTypeMappings="@(StaticWebAssetContentTypeMapping)"
>
<Output TaskParameter="Endpoints" ItemName="StaticWebAssetEndpoint" />
</DefineStaticWebAssetEndpoints>
</Target>
```
为了测试如下的代码,可以在项目中新建一个`Directory.Build.targets`文件,将上述的内容复制进去进行测试,当然别忘了用`<Project>`标签包裹这一切。

BIN
source/posts/aspnetcore-swa/image-20251231225433184.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,113 @@
---
title: 人生代码大作业初体验
date: 2022-07-27T11:34:49.0000000
tags:
- 杂谈
---
在大学也呆了一年了,终于遇上了第一个需要多人合作的写代码项目。从四月底分组完成,任务部署下来到七月初接近尾声,在这两个多月的时间里,也算是经历了不少,学到了不少。
<!--more-->
## 项目背景
项目的主体是一个公交车的自动调度系统。公交车行驶在一个环形的轨道上,站点均匀的分布于其上;系统需要支持在每个站点上创建顺时针上车、逆时针上车和下车三种请求,并调度公交车按照一定的策略前往这些站点处理请求。
> 在一开始我还以为这个策略需要自己优化,通过比较不同组之间调度完成请求需要的时间来决定优劣,想着还有点可怕,后来明确策略的规则是由老师指定。
>
> 不过要是真的按照策略的优化来比较程序之间的优劣,想想还是很有趣的一件事情,不过用在一个计算机专业第一节专业课的大作业上显得就有点那啥了。
项目要求使用C语言实现一个控制台中的核心版再采用 `C/C++`相关的图形化框架实现一个动画版。
整个小组由三个人组成。剩下的两个人有 `Dev C++`的经历对于C语言的语法还算是比较熟悉。
## 项目的管理
### 构建管理工具
在开始一个项目之前,应该先选择一个合适的技术栈。在选择的时候,不能光考虑自己平时的习惯和技术的先进性,还得考虑组内所有人的合作效率,不能让代码就在你一个人的电脑上能跑起来,同时这个技术得是所有人都比较熟悉的,不能说你在那里吭哧吭哧的都把代码写完了,其他人还搁那看文档和教程。
`C`语言项目的管理上,我首先排除了一个文件打天下的“传统”生产模式。这个方案的优点是所有人的十分熟悉,不需要任何额外的技术,也对 `C`语言的IDE没有任何的要求不管你是用初学者的常用的 `Dev C++`还是宇宙第一IDE `Visual Studio`都可以打开这个单独的文件然后编译运行。但是这个方案也就这一个优点了。且不论手工合并很多人撰写代码的复杂繁琐光是一个长达1000行的文件就看得人头疼欲裂了。
其次我排除了 `Visual Studio`虽然VS也算是一个开箱即用的开发平台。第一我自己对 `Visual Studio`不熟悉如果在合作开发中遇到了一些VS的问题我可能也不知道该如何解决。第二我们验收使用的OJ平台采用的是 `GCC`编译器,不同于 `MSVC`编译器,虽然编译器行为不同的情况很少遇见,但为了避免这些一遇到就是纯玄学的问题,我还是采用 `MinGW64 GCC`编译器,这个编译器也算是我常用的编译器。
最后我选择了利用 `cmake`组织项目,`MinGW64 GCC`作为编译器的方案。虽然 `cmake`还是一个比较新的玩意儿,但是只要编译的模板在一开始写好了,就基本上不用再修改了,并不需要所有人都熟悉 `cmake`。其次,`cmake`对于IDE没有依赖几乎所有的主流 `C/C++ `IDE都支持利用 `cmake `管理 `C/C++`项目。我这次的推荐IDE是 `VSCode `,如果使用 `cmake`进行项目的管理,只需要安装一个插件就可以进行开发了,避免了很多麻烦的问题。
### Git
本次开发中老师提供了 `gitlab`作为代码托管平台,因此在开发过程中我们顺理成章的采用 `Git`作为版本管理的工具,~~虽然组里的同学完全不会。~~
为了避免对于 `Git`的学习占用了过多的开发时间,我没有在项目开始之前花费大量的时间指导他们学习 `Git`,而是再讲解了基本的 `Git`操作流程,比如 `commit push pull`之后就直接开始项目的开发,让大家在开发项目的过程中逐渐熟悉这个工具的使用,在工作中遇到问题再解决对应的问题。同时为了避免合并冲突等比较复杂的情景在早期出现,在最开始开发的时候我让大家都在不同的文件上工作,在后期必须要在同一文件上工作时再来解决合并的问题。
> 我一直以为,在开发的过程中学习才是进步最快的方式
而且采用 `Git`还有一个好处,采用 `Github``Insight`功能可以轻松的看出大家的贡献值()。
![img](1.webp)
## 一些技术上的收获
### 面向对象永远的神
在这次的开发过程中核心版要求完全使用C语言进行开发而在动画版中则可以采用C++进行开发。在编写动画版的代码时,我没有简单的重用核心版的代码,而是利用面向对象的方法完全重写了一遍,虽然重写耗费了我不少的时间,但是在重写了之后,程序中不再使用全局变量;控制公交车通过抽象为三个虚函数,不同的策略对这三个虚函数进行不同的实现来达到不同的调度效果,这样核心的控制流只用编写一遍,个人认为优于核心版中几种策略的控制流虽然大体相似但是在细节上均有不同。
```cpp
/**
* 获得当前公交车应该前进的方向
* @return 公交车前进的方向
*/
virtual int GetBusDirection() = 0;
/**
* 获得公交车在当前指定的策略下应该处理的请求
* @return 请求指针
*/
virtual bus_query_t *GetTargetQuery() = 0;
/**
* 获取公交车现在可以顺便处理的请求
* @return 请求指针
*/
virtual bus_query_t *HandleBTWQuery() = 0;
```
> 然而我还是没有对象
### Qt
具体参见[初学Qt的一点小笔记](https://rrricardo.top/blog/2022/07/01/qt-learning/#more)`QProperyAnimation`那个的多重继承是真的惊艳到我了,感觉在 `C++`这样复杂又精妙的语言面前我还有很多可以学习的地方。
### 单元测试
在这次的开发过程中我引入了 `Google Test`单元测试框架以进行单元测试,虽然并不是所有的模块都编写了对应的测试集,但是单元测试的引入真的帮助我们在开发初期解决的很多的小问题。
```cpp
TEST(rail, CreateRails)
{
rail_node_t *head = CreateRails(10, 10);
rail_node_t *p = head;
for(int i = 1; i <= 10; i++)
{
EXPECT_EQ(p->id, i);
EXPECT_EQ(p->next_node_distance, 10);
EXPECT_EQ(p->last_node_distance, 10);
p = p->next_node;
}
p = head->last_node;
for(int i = 10; i >= 1; i--)
{
EXPECT_EQ(p->id, i);
EXPECT_EQ(p->next_node_distance, 10);
EXPECT_EQ(p->last_node_distance, 10);
p = p->last_node;
}
}
```
### 脚本
在开发的过程中总有一些重复性的简单工作,这时 `Python`这些脚本语言的妙处就展示出来了。由于核心版是采用线上的OJ平台进行测试在提交的时候代码为单文件于是我编写了一个[脚本](https://github.com/jackfiled/auto_bus/blob/master/main.py)来帮助我完成这个工作。

BIN
source/posts/big-homework/1.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,453 @@
---
title: 建立博客过程的记录
date: 2022-04-08T11:52:32.0000000
tags:
- 技术笔记
---
当我已经在Python的浩瀚大海遨zhengzha了半个暑假后我决定尝试一下传说中程序员专用的学(zhuang)习(bi)手(fangfa)段(fa)——建立自己的个人博客。作为一个半懂不懂的Python程序员心中冒出的第一个想法自然是采用Python的Django作为开发自己的个人博客的手段。然而在阅读了[用Django搭建个人博客](https://www.dusaiphoto.com/article/2/)等的其他人搭建这类动态博客的过程记录之后我便义无反顾的转向了采用javascript开发的博客框架[Hexo](https://hexo.io)<del>说好的Python信仰呢</del>。无他,唯简单尔。
<!--more-->
## 安装需要的程序
### 安装javascipt的运行环境
Hexo作为一个基于javasrcipt的博客框架第一步自然是安装运行Javascript的环境。
Node.js就是几个基于Chrome V8引擎的Javascript运行时环境。这是一个异步事件驱动的Javascript运行时环境。同时Node还带有一个和`pip`功能类似的包管理工具`npm`,使我们可以方便的安装其他人开发的功能扩展包。我们就使用这种方便的方式安装`hexo`
访问[node.js官方网站](https://nodejs.org/en/)下载了node.js的14.17.4 LTS版本的安装包下载完成后安装。在Powershell中输入
```
node -v
```
若能显示出node的版本
```
v14.17.4
```
便说明node.js安装成功。
再输入
```
npm -v
```
显示出npm的版本
```
7.20.3
```
便说明npm也安装完成。
>这里要特别说明npm的更新快于node可能在不久之后npm的版本就不是7.20.3了
>
>同时可以使用`npm install -g npm`方便的升级npm
### 安装Hexo
在Power shell中输入
```
npm install -g hexo-cli
```
>这里的-g代表global即为全局安装Hexo如果像我一样初次使用npm建议安装所有的包时都加上-g。
npm的默认安装源在国外如果在安装过程中遇到网络问题可以像我们使用其他的包管理器一样换为国内源进行安装。在国内比较出名的npm镜像源是由部分淘宝程序员维护的淘宝源。
> 2022年淘宝源的域名由https://npm.taobao.org更改为https://npmmirror.com。
#### 临时使用淘宝源作为下载方式
在利用npm安装npm包时使用
```
npm --registry https://registry.npmmirror.com install
```
可以在本次下载包时采用淘宝源作为下载地址。
#### 将下载地址设置为淘宝源
```
npm config set registry https://registry.npmmirror.com
```
这样设置以后,每次下载包时都会从淘宝的服务器下载。
#### 使用cnpm下载
cnpm是国内一些热心于开源的程序眼开发的一个node包作用和node.js自带的npm完全一样不过默认使用淘宝源下载。在下载cnpm之后就可以方便的在国内源和国外源之间切换当使用国内源时使用
```bash
cnpm install
```
使用国外源时使用
```
npm install
```
这就避免了在需要使用国外源时来回切换的麻烦。<del>虽然我应该不会用到国外源</del>
开始时使用
```
npm install cnpm -g
```
安装cnpm
然后使用
```
cnpm install Hexo-cli -g
```
安装Hexo博客框架。
再输入
```
hexo -v
```
验证安装是否完成。
我这里的输出是
```
hexo-cli: 4.3.0
os: win32 10.0.19043
node: 14.17.4
v8: 8.4.371.23-node.76
uv: 1.41.0
zlib: 1.2.11
brotli: 1.0.9
ares: 1.17.1
modules: 83
nghttp2: 1.42.0
napi: 8
llhttp: 2.1.3
openssl: 1.1.1k
cldr: 39.0
icu: 69.1
tz: 2021a
unicode: 13.0
```
## 初次使用Hexo
### 创建博客
进入一个我们准备用来设置博客的文件夹,在终端中输入
```
Hexo init blog
```
Hexo会以blog为名称创建一个博客文件夹这个文件夹的内容为
![文件夹截图](1.webp)
`node_modules`文件夹是Hexo需要用到的一些npm依赖包的存放地址`public`文件夹下是由Hexo渲染产生的静态博客文件`scaffolds`文件夹是博客用到的模板文件,在默认情况下应该有`draft.md`,`page.md`,`post.md`三个模板文件。`themes`是Hexo中可以使用的主题文件。主题也是Hexo一个非常方便的设计我们可以方便使用其他人编写的Hexo Themes让自己的博客在不同的风格之间变换。`source`文件夹就是存放我们写作的博客的地方。一般这里面会有两个子文件夹,`_draft`, `_posts`。我们在里面在创建一个`img`文件夹,把自己的头像图片和网站的图标文件都放在里面,在之后的设置的时候使用。
在终端中输入
```bash
hexo server
INFO Validating config
INFO Start processing
INFO Hexo is running at http://localhost:4000/ . Press Ctrl+C to stop.
```
会在本地运行Hexo自带的一台静态博客服务器。我们用浏览器访问http://localhost:4000, 就可以看见Hexo博客的初始界面
![初始截图](2.webp)
这便说明安装成功了,~~可以开香槟了~~
### 写作
在终端中输入
```bash
hexo new "文章标题"
```
> 我已经在设置中设置,不指定模板是自动生成草稿。具体设置见下一节。
然后就会在前面提到的`_draft`文件夹下创建一个markdown文件和一个同名的资源文件夹在资源文件夹下放置在文章中会用到的图片。接下来使用一款适合自己的markdown编辑器就可以开始文章的写作了。
> 我使用的markdown编辑器是[Typora](https://typora.io/),但是这个软件在更新到正式版之后就开始收费了,不过我们可以在[这里](https://typora.io/releases/all)找到版本小于1.0.0的beta版本使用。
为了方便的在写作时插入图片,我下载安装了`hexo-asset-image`这个Hexo插件但是由于这个插件总是在我的电脑上犯病我自己做了一点修改放在了[我的github上](https://github.com/jackfiled/hexo-asset-image)。
通过
```bash
npm install git+https://github.com/jackfiled/hexo-asset-image --save
```
来安装我修改之后的包。
这样在typora中写作的时候先通过格式-图像-设置图片根目录为hexo自动生成的资源文件夹在需要插入图片时通过
```markdown
![example](example.png)
```
这种方式来插入图片,其中`example.png`图片在资源文件夹下。这样在typora中可以正确显示在hexo渲染出来的网页中也可以正确的显示。
在完成写作之后,使用
```bash
hexo publish "文章标题"
```
将文章发布把markdown文件和资源文件夹从`_draft`文件夹移动到`_post`文件夹。
这时使用
```
hexo server
```
就可以看见我们完成的博客文章了。
### 设置主题
为了选择一个恰当而合适的主题,以期避免可能引起的一些不必要的误会和不便,我们设立一个跨部门的多方委员会,用以充分考虑各方的意见,同时选择专门的专家主持流程严谨的研究,充分考虑科学界的意见。通过多次多方的协调会议,在完全理解各方需求的之后,委员会提供了多种选择以供我们选择。
> 汉弗莱附身了属于是
简而言之,我看了几个博客,下载了几个主题测试,然后选择了[yilia-plus](https://github.com/JoeyBling/hexo-theme-yilia-plus)。我比较看重的这个主题的原因是他比较简洁。
在blog文件夹中的themes文件夹`git clone`我们选择好的主题在blog文件夹下的`_config.yaml`中设置主题
```yaml
theme: yilia-plus
```
然后我们在运行
```bash
hexo server
```
就可以看见我们的主题设置已经生效。
### 调整设置文件
Hexo在一般情况下有两个配置文件我们会经常用到blog根目录下的`_config.yaml`, 我们下载的主题文件夹下的`_config.yaml`
#### Hexo的设置
```yaml
# Site
title: Ricardo的博客
subtitle: '奇奇怪怪东西的聚居地'
description: ''
keywords:
author: Ricardo Ren
language: zh-CN
timezone: ''
```
第一部分的网站设置部分,根据自己的需求修改
```yaml
# URL
## Set your site url here. For example, if you use GitHub Page, set url as 'https://username.github.io/project'
url: http://rrricardo.top/blog
permalink: :year/:month/:day/:title/
permalink_defaults:
pretty_urls:
trailing_index: true # Set to false to remove trailing 'index.html' from permalinks
trailing_html: true # Set to false to remove trailing '.html' from permalinks
```
第二部分在url的地方填写自己的博客的地址。
第三部分`Directory`中我没有修改任何玩意儿。
第四部分`Writing`中我把默认使用的模板设置为草稿, 再将`post_asset_folder`设置为true,这样在使用`hexo new `命令使就会再md文件所在的目录创建一个同名的资源文件夹把我们文章中会使用到的图片放在里面。
```yaml
default_layout: draft
post_asset_folder: true
```
剩下的大部分我们就可以不用修改了。
#### yilia-plus的设置
> 由于这个主题是国人开发的,配置文件都有详细的中文注释,按着注释走就完事儿了。
## 将博客部署到云服务器
### 设置deploy
Hexo做为一个静态的博客框架可以将整个博客网站直接渲染为静态页面,我们可以执行
```
hexo g
```
Hexo就会在`public`文件夹下生成整个博客的静态界面,我们只用在服务器上放置这些文件就可以了。
这里使用hexo提供的deploy功能来简化本地同步到git仓库的过程。
在终端执行下列命令安装git部署插件
```bash
npm install hexo-deployer-git --save
```
在根目录下的`_config.yaml`中设置
```yaml
# Deployment
## Docs: https://hexo.io/docs/one-command-deployment
deploy:
type: git
repo: git@gitee.com:ricardo-ren/blog-deploy.git
branch: master
message: Site Update {{now('YYYY-MM-DD HH:mm:ss')}}
```
设置中的message信息可以按自己的喜好设置。
> 如果使用这种方式记得先在git上创建远程仓库
设置完成后执行
```bash
hexo deploy
```
可能会提示没有设置用户名和邮箱
```bash
cd .deploy_git
git config ...
```
这里进入的`.deploy_git`实际上就是git仓库在本地的位置。
### 服务器clone
在服务器上适当的位置执行
```bash
git clone git@gitee.com:ricardo-ren/blog-deploy.git
```
这里将作为博客网站的根目录。
### nginx设置
在云服务器上我使用nginx作为反向代理服务器。由于nginx也是一个不错的静态资源服务器hexo博客也就用nginx作为服务器了。
首先安装nginx
```bash
sudo apt install nginx
```
然后编写nginx的配置文件在`/etc/nginx/nginx.conf`
```nginx
user root;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
# gzip_vary on;
# gzip_proxied any;
# gzip_comp_level 6;
# gzip_buffers 16 8k;
# gzip_http_version 1.1;
# gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
##
# Virtual Host Configs
##
#这两行我注释了,否则配置文件貌似不会生效
#include /etc/nginx/conf.d/*.conf;
#include /etc/nginx/sites-enabled/*;
# server
server {
listen 443 ssl;
server_name rrricardo.top;
# ssl settings
ssl_certificate /etc/letsencrypt/live/rrricardo.top/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/rrricardo.top/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
location / {
return 301 https://rrricardo.top/blog/;
}
location /blog/ {
root /home/rcj/website/;
index index.html index.htm;
}
}
server {
listen 80;
server_name rrricardo.top;
return 301 https://$server_name$request_uri;
}
}
```
nginx大部分的默认设置都没有改动指设置了Let's Encrypt提供的HTTPS证书以提供HTTPS服务博客网站挂载在443端口的`/blog/`下当访问443端口的`/`时会301重定向到`/blog/`上。
> 安装Let's Encrypt的服务主要参考[Let's Encrypt](https://letsencrypt.org/zh-cn/getting-started/),[Certbot Instructions | Certbot (eff.org)](https://certbot.eff.org/instructions?ws=nginx&os=ubuntufocal),还有[免费 https 证书Let's Encrypt申请与配置 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/21286171)。
### 网站备案
按照你的云服务器提供商的指南进行就可以了,我的备案过程还算比较顺利。
## 后记
从2021年9月15日博客仓库的首次提交到这篇博客完成已经过去了七个月的时间经过七个月不断的修补和改进我的博客终于也算是有了一个博客的样子。
一路上读了许多人的博客,已经无法一一指出,在此一并表示感谢。
文中也不免有许多疏漏之处,因时间飞逝,当时遇到的一些问题也无法一一记录,还请诸位读者海涵。

BIN
source/posts/build-blog-record/1.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/build-blog-record/2.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,233 @@
---
title: 交叉编译.NET到RISC-V平台
date: 2024-08-25T15:41:05.9519941+08:00
tags:
- dotnet
- 技术笔记
---
我们编译是这样的,在本平台上编译只要敲三条命令就好了,而交叉编译要考虑的就很多了。
<!--more-->
这次我们打算在`x86_64`平台上交叉编译`.NET``riscv64`平台上。
首先从相关的[进度跟踪页面](https://github.com/dotnet/runtime/issues/84834)显示,.NET移植到RISC-V的进度还远远没有完成但是在整个SDK中除了AOT编译器的部分都可以在RISC-V平台上编译了。
## 环境准备
我们构建的环境是Arch Linux因此依赖包的安装使用`pacman`进行。综合[.NET官方文档](https://github.com/dotnet/runtime/blob/main/docs/workflow/requirements/linux-requirements.md)给出的信息和Arch Linux官方打包的脚本所需要安装的软件包如下
| 包名 | 备注 |
| ------------- | ------------------------------------------------------------ |
| bash | |
| clang | |
| lld | |
| cmake | |
| git | |
| icu | 第一次看见这个名词就想吐槽谁TM想得到重症监护室会是一个全球化支持库 |
| inetutils | 常见的网络工具库,官方文档没有但是构建脚本有 |
| krb5 | 一个网络通信认证库?不懂 |
| libgit2 | |
| libunwind | 解析程序运行堆栈的魔法工具 |
| libxml2 | |
| lldb | |
| llvm | |
| lttng-ust2.12 | 又是一个跟踪运行的魔法工具 |
| openssl | |
| systemd | |
| zlib | |
### 交叉编译工具链
在正式开始编译.NET之前先学习如何搭建一套C/C++的交叉编译工具链。
通常一份GNU工具链只能针对一个平台进行编译但是LLVM工具链是一套先天的交叉编译工具链例如对于`llc`工具,使用`llc --version`命令可以看见该编译器可以生成多种目标平台上的汇编代码:
![image-20240824120646587](./build-dotnet-from-source/image-20240824120646587.webp)
在使用`clang++`时加上`--target=<triple>`指定目标三元组就可以进行交叉编译。
但是直接使用`clang++ --target=riscv64-linux-gnu hello.cpp -o hello`时会爆出一个奇怪的找不到头文件错误:
```cpp
// File: hello.cpp
#include <iostream>
int main()
{
std::cout << "Hello, world!" << std::endl;
return 0;
}
```
![image-20240824121425007](./build-dotnet-from-source/image-20240824121425007.webp)
看样子交叉编译也不是开箱即用的。最开始我们猜想系统提供的LLVM工具链没有被配置为交叉编译因此尝试在本地自行编译一套LLVM工具链。
首先从[Github Release](https://github.com/llvm/llvm-project/releases)上下载最新的`llvm-project`源代码并解压到本地文件夹中。这里126M的压缩文件可以解压出一个1.8G大小的源代码文件夹。创建一个`build`文件夹,在该文件夹使用如下的配置进行编译,在配置中使用`LLVM_TARGETS_TO_BUILD`选择启用`X86``RISCV`的支持。
```bash
cmake ../llvm-project.src/llvm \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DLLVM_TARGETS_TO_BUILD="X86;RISCV" \
-DLLVM_ENABLE_PROJECTS="clang;lld;clang-tools-extra"
make
sudo make install
```
编译之后的成果会安装到`/usr/local/`目录下,而在`$PATH`环境变量中`/usr/local`位置将在`/usr`目录之前因此调用时将会优先调用我们自行编译的LLVM工具链而不是系统中安装的LLVM工具链。
![image-20240824134158262](./build-dotnet-from-source/image-20240824134158262.webp)
但是使用这套编译工具链仍然会爆出和之前一样的问题。说明这并不是系统安装LLVM工具链的问题。仔细一想也确实这里提示找不到对应的头文件应该是找不到RISC-V架构之下的头文件——这里的也是交叉编译的主要问题所在虽然LLVM工具链宣称自己是原生支持交叉编译的但是没人宣称说标准库和头文件是原生的。这里我们就需要一个根文件系统来提供这些头文件和各种库文件。
### 生成根文件系统
在.NET的构建文档中提供了一个自动生成头文件的脚本但是这个脚本似乎强依赖某个U开头的发行版身为Arch神教信徒的我似乎没有办法使用。直接使用预构建好的镜像又屏蔽了太多的技术细节感觉也不太好。因此打算尝试使用[arch-riscv](https://mirror.iscas.ac.cn/archriscv/)提供的移植Arch Linux系统作为根文件系统。
首先使用移植之后的根文件系统构建一个`archriscv`镜像:
```Dockerfile
FROM archriscv AS bootstrap
COPY etc /rootfs
COPY bootstrap/pacstrap-docker /usr/local/bin/
RUN pacstrap-docker /rootfs base
RUN rm /rootfs/var/lib/pacman/sync/*
FROM scratch AS root
COPY --from=bootstrap /rootfs /
COPY etc /etc
LABEL org.opencontainers.image.title="Arch Linux RISC-V"
LABEL org.opencontainers.image.description="This is an Arch Linux port to the RISC-V architecture."
ENV LANG=en_US.UTF-8
RUN ldconfig && locale-gen
RUN pacman-key --init && \
pacman-key --populate && \
bash -c "rm -rf etc/pacman.d/gnupg/{openpgp-revocs.d/,private-keys-v1.d/,pubring.gpg~,gnupg.S.}*"
CMD ["/usr/bin/bash"]
```
虽然这个镜像是一个自举的镜像,给出这个构建文件似乎没有什么用处(笑)。再在这个镜像的基础上新建一层镜像安装各种.NET的依赖项。
```dockerfile
FROM archriscv
RUN pacman -Syyu --noconfirm bash clang cmake git icu inetutils \
krb5 libgit2 libunwind libxml2 lldb llvm lttng-ust2.12 \
openssl systemd zlib
```
构建这个镜像,再将这个镜像根目录下的所有文件拷贝出来。
```bash
docker build . --platform linux/riscv64 -t archriscv:base-devel
mkdir rootfs
cid=$(docker run -d --platform linux/riscv64 archriscv:base-devel)
sudo docker cp $cid:/ rootfs
sudo chown $USER:$USER -R rootfs
```
新建一个`runtime-build`文件夹,使用下面的指令在`rootfs`文件系统中构建`libcxx``compiler-rt`
> `libcxx`和`compiler-rt`不是常规交叉编译需要的,而是编译.NET所需要的。
```bash
export TARGET_TRIPLE="riscv64-linux-gnu"
export CLANG_MAJOR_VERSION=18
export ROOTFS_DIR=<ROOTFS>
cmake -S ../llvm-project.src/runtimes \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_ASM_COMPILER=clang \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_ASM_COMPILER_TARGET="$TARGET_TRIPLE" \
-DCMAKE_C_COMPILER_TARGET="$TARGET_TRIPLE" \
-DCMAKE_CXX_COMPILER_TARGET="$TARGET_TRIPLE" \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
-DCMAKE_SYSROOT="$ROOTFS_DIR" \
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=lld" \
-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM="NEVER" \
-DLLVM_USE_LINKER=lld \
-DLLVM_ENABLE_RUNTIMES="libcxx;compiler-rt" \
-DLIBCXX_ENABLE_SHARED=OFF \
-DLIBCXX_CXX_ABI=libstdc++ \
-DLIBCXX_CXX_ABI_INCLUDE_PATHS="$ROOTFS_DIR/usr/include/c++/14.2.1/;$ROOTFS_DIR/usr/include/c++/14.2.1/riscv64-unknown-linux-gnu/" \
-DCOMPILER_RT_CXX_LIBRARY="libcxx" \
-DCOMPILER_RT_STATIC_CXX_LIBRARY=ON \
-DCOMPILER_RT_BUILD_SANITIZERS=OFF \
-DCOMPILER_RT_BUILD_MEMPROF=OFF \
-DCOMPILER_RT_BUILD_LIBFUZZER=OFF \
-DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON \
-DCOMPILER_RT_INSTALL_PATH="/usr/local/lib/clang/$CLANG_MAJOR_VERSION"
make -j20
sudo cmake --install . --prefix "$ROOTFS_DIR/usr"
```
在构建指令中需要根据安装的`gcc`版本调整`_DLIBCXX_CXX_ABI_INCLUDE_PATHS`的路径。
完成所有上述的工作之后,回到我们最开始的你好世界样例,使用下面这行神秘的代码进行编译:
```bash
clang++ --target=riscv64-linux-gnu --sysroot=$ROOTFS_DIR -fuse-ld=lld hello.cpp -o hello
```
这次编译不会出现问题,上面指定的三个参数依次为指定目标三元组、指定根文件系统的位置和指定使用`lld`作为链接器。使用Docker镜像进行测试确认编译之后的二进制文件可以正常运行。
### 复盘
在正式开始下一步之前,我们先复盘一下在搭建交叉编译环境时我们都做了什么:
- 使用`LLVM_TARGETS_TO_BUILD`编译了一套新的LLVM
- 将安装了基础依赖包的`archriscv`导出作为根文件系统,
- 使用该根文件系统在该根文件系统中编译了`libcxx``compiler-rt`两个库。
这三步也带来了三个问题:
1. Arch Linux自带的LLVM工具链难道不能交叉编译吗
2. Arch Linux 官方提供的`riscv64-linux-gnu-gcc`包能够作为根文件系统吗?
3. 能够在上述的根文件系统中安装我们需要的`libcxx``compiler-rt`两个库吗?
第一个问题的回答是Arch Linux安装的LLVM工具是可以交叉编译的。虽然在Arch Linux官方构建LLVM工具链的[构建脚本](https://gitlab.archlinux.org/archlinux/packaging/packages/clang/-/blob/main/PKGBUILD?ref_type=heads)中没有使用`LLVM_TARGETS_TO_BUILD`参数,但是这个参数的默认值是`all`。这一点我们也可以通过实验来验证。
![image-20240824153514149](./build-dotnet-from-source/image-20240824153514149.webp)于是回到编译`llvm`的目录下执行`cat install_manifest.txt | sudo xargs rm`
第二个问题的回答可以使用实验来验证,首先安装`riscv64-linux-gnu-gcc`,然后将根文件系统的位置设置为`/usr/riscv64-linux-gnu`,重新编译上面的你好世界样例。编译之后可以正常执行。
第三个问题的回答是还是新建一个根文件系统罢,随便往系统目录里面写东西感觉是一个不太好的习惯。
## 正式编译
首先进入克隆代码的目录,运行初始化脚本。
```bash
cd dotnet
./prep-source-build.sh
```
设置根文件系统的目录,这里仍然使用从安装了`base-devel`的Docker容器中导出并自行编译了`compiler-rt``libcxx`的根文件系统。
```bash
export ROOTFS_DIR=<rootfs>
```
然后使用下面这条神秘的命令开始交叉编译:
```bash
./build.sh -sb --clean-while-building /p:TargetOS=linux /p:TargetArchitecture=riscv64 /p:Crossbuild=true /p:BuildArgs="/p:BundleNativeAotCompiler=false"
```
上面的第一个参数是指定了`source-build`选项第二个参数指定了在编译的过程中清理不需要的文件以节省硬盘空间后面的几个MSBUILD参数则是指定为RISC-V架构上的Linux系统构建并且不构建AOT编译器。
但是现在的.NET在RISC-V平台上还是废物一个甚至连`dotnet new`都跑不过,下一步看看能不能运行一下运行时的测试集看看。
![image-20240824214145759](./build-dotnet-from-source/image-20240824214145759.webp)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,107 @@
---
title: C项目中有关头文件的一些问题
date: 2022-05-08T11:35:19.0000000
tags:
- 技术笔记
- C/C++
---
最近在完成一门`C`语言课程的大作业,课设老师要求我们将程序分模块的开发。在编写项目头文件的时候,遇到了一些令本菜鸡大开眼界的问题。
<!--more-->
## 头文件相互包含问题
### 问题
我项目的结构大致如图所示:
![](1.webp)
`include`的头文件目录下有两个头文件,`rail.h``bus.h`,这两个头文件分别定义了两个结构体`rail_node_t``bus_t`
但在这两个结构题的定义中,我互相使用了指向对方结构体的指针。
```C
/*rail.h的内容*/
#include "bus.h"
struct {
...
bus_t *bus;
...
} rail_node;
typedef struct rail_node rail_node_t;
```
```C
/*bus.h的内容*/
#include "rail.h"
struct {
...
rail_node_t* rail_node_pos;
...
} bus;
typedef struct bus bus_t;
```
于是在编译的时候,编译器就会报`rail_node_t``bus_t`这两个结构体未定义的错误。
### 解决
这个问题解决起来也非常的容易,只要修改其中一个结构体的定义就可以了。在以后的设计中注意不要出现这类相互包含的结构体。
> 而且一般这种时候虽然IDE的静态检查不会报错但是自动补全却会失效。所以当你发现你的IDE出现一些奇怪行为的时候就要格外小心了。
## 自己定义的头文件和内部头文件命名冲突的问题
### 问题
在项目中我引入了谷歌的单元测试框架[GTest](https://github.com/google/googletest)。但是在编译测试程序的时候遇到了一些困难。
项目的`test`文件夹下是单元测试文件夹,但是在编译的时候会报错
![](2.webp)
大意就是在一个google test内部的头文件中有几个函数找不到定义这个函数都位于`io.h`这个头文件中。
在一开始我以为是平台的兼容性问题但是在我电脑的其他项目中引用这个库都没有问题。在一开始我以为是google test作为一个为`C++`设计的单元测试库在我的`C`项目中出现了不兼容的情况,于是我在设计一个假的单元测试
```C++
#include "gtest/gtest.h"
#include "gmock/gmock.h"
using ::testing::Return;
using ::testing::AtLeast;
using ::testing::Exactly;
using namespace testing;
TEST(test, test)
{
EXPECT_EQ(1, 1);
}
```
不测试任何我编写的库,而只是验证单元测试框架是否能正确运行。但是这个单元测试仍然无法通过编译,报错和之前的一样。
于是我便打开了编译中出错的`gtest-port.h`文件,发现在预处理的过程中头文件的替换出现了问题:在我自己的头文件中也有一个名叫`io.h`的头文件,负责项目中的输入输出,而在预处理的过程中预处理器用这个头文件代替了标准库中的`io.h`,但在我自己的头文件中自然没有测试库需要的函数了。
> 在找bug的过程中还有比较玄学的事情如果我把我库里的头文件一个个的添加就可以编译成功但是如果在第一次编译成功之后再次`cmake ..`,重新生成编译文件,再编译就失败了。这个玄学现象让我迷惑了很久。
### 解决
重新命名模块即可。在后续的模块设计中注意命名。
> 这里还有一个小插曲不要轻易相信IDE提供的重构功能。在这里`CLion`这个IDE就在我重命名头文件的时候把`gtest-port.h`中对`io.h`的引用也重构了。真是智能!~~此处应有流汗黄豆~~
## 后记
只有在实际的开发中才能学到这些教训啊!

BIN
source/posts/c-include-problems/1.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/c-include-problems/2.webp (Stored with Git LFS) Normal file

Binary file not shown.

68
source/posts/cncc-2024.md Normal file
View File

@@ -0,0 +1,68 @@
---
title: 2024中国计算机大会
date: 2024-11-03T14:06:36.4212070+08:00
tags:
- 杂谈
---
2024年的中国计算机大会于10月24日到10月26日在浙江省金华市东阳市横店镇举办而鄙人在下不才我有幸受到实验室资助前去参观学习。
<!--more-->
首先开幕式镇楼。
![image-20241102212738598](./cncc-2024/image-20241102212738598.webp)
## 学术上
大会每天的日程是上午的大会特邀报告和大会论坛,下午的各个分论坛讨论。老实说,大会上午的报告和论坛我都没有特别感兴趣,因此这里将重点放在我参加的三个分论坛上。
### AI时代的异构融合操作系统聚散终有时融合亦有期
第一个报告是华为庞加莱实验室秦彬娟老师的《异构智算时代的操作系统演进》。报告高屋建瓴从比较宏观的角度上介绍了当前异构融合操作系统诞生的背景、发展的方向。在报告中重点介绍了一种异构融合操作系统的设计思路通过三层架构基于互联池化技术构建AI时代的融合算力系统。系统中的三层包括1池化基础底层包括多设备的融合和池化设备虚拟化2异构融合核心子系统例如异构融合调度系统、异构融合内存和异构融合存储系统3异构核心服务。总的来说这个报告在一定程度上勾勒出了未来一个异构融合操作系统应有的各项功能但是显然这一操作系统的实现还存在着明显的困难。
![image-20241102211959206](./cncc-2024/image-20241102211959206.webp)
下面一个报告是较为有干货的报告北京航空航天大学刘瀚骋老师的《异构融合OS及多样性内存管理框架》。报告中介绍了一个称作`FMMU`的系统是对于异构融合操作系统中内存管理系统的探索。报告中首先介绍了内存池化技术对于异构融合操作系统的重要性指出分布式共享内存Distributed Shared Memory可能是实现内存池化技术的未来。然后介绍了将部分内存管理中的计算卸载到可编程网络硬件中来加速分布式内存访问的新思路。最后在报告中提到了内存管理技术如何解决错误预测和错误回复的问题。虽然在听的时候没太注意但是现在总结的时候才发现这个报告的思路似乎有点混乱尤其是最后一点和内存管理系统并没有什么直接的关系而且这个内存管理系统似乎不是**异构系统**的内存管理,反而是分布式系统的内存管理。不过总的来说,这个报告还是非常实际的,介绍了不少当前异构融合操作系统中的内存管理面临的问题和解决问题的探索。
![image-20241102212355390](./cncc-2024/image-20241102212355390.webp)
第三个报告是国防科技大学李东升老师的《异构计算环境下的分布式深度学习训练》。报告首先从李老师的主业——并行计算起手,介绍了深度学习训练过程中主要的各种并行方法,例如数据并行、模型并行和混合并行等,指出目前大模型的并行训练存在着计算/存储/通信难的问题。因此提出了一个智能模型训练并行任务划分方法1基于符号算子的计算图定义方法2面向Transformer模型的流水线并行任务划分方法3异构资源感知的流水线并行任务划分方法。然后针对分布式模型训练中通信调度存在的通信墙、数据依赖关系复杂等的问题提出综合词嵌入表的稀疏通信调度技术、流水线并行的P2P通信调度技术、模型计算的统一操作执行引擎和网络链路感知的通信执行引擎的通信调度技术。最后提到了智能模型训练 的内存优化技术针对现有重计算技术re-computing和存储交换swapping技术存在的问题提出了一种面向大型智能模型训练的细粒度内存优化方法`DELTA`
最后一个报告是上海交通大学杜冬冬老师的《软硬芯异构融合操作系统的多个维度》。报告伊始杜老师就抛出一个问题操作系统的演进应该是提供新的抽象还是兼容现有的抽象在回答这个问题之前杜老师首先介绍他们一个异构融合操作系统的设计思路层OS架构的思路通过设置两个层次——全局OS和本地OS全局OS在本地OS的基础上提供一层跨`XPU`的能力。杜老师设计的这个系统称作`XPU-Shim`,在设计这个系统时就面对着前面的问题,是提供新的抽象还是兼容现有的抽象。`XPU-Shim`的回答是兼容现有的抽象在底层的CXL、UB等内存语义总线的基础上实现了传统的Socket抽象提供了低时延、高吞吐的协同能力。在操作系统的抽象问题之外杜老师还就云上GPU应用的启动时延问题进行了讨论深入解释了通过状态复用完全跳过初始化阶段从而加速应用冷启动过程的思路。
Plane讨论没有参加。
### 编译系统前沿技术与应用
第一个报告是清华大学陈文光老师的《神经网络全同态编译器》。这个报告可以说证明了“编译技术的人才活跃在各行各业”,报告中的主要内容就是编译技术如何助力机密计算中的全同态加密应用在神经网络的推理中。全同态加密算法实现了“数据可用不可见”的概念,允许程序直接在密文上进行乘法和加法运算,但是限制也是只能进行加法和乘法运算,而且过多的乘法操作会造成计算之后解密失败。该编译器成为`ANT-ACE`首先通过设计新的五层中间表示IR实现了自动化全同态加密程序生成和面向性能的优化设计在实现基本的编译工作之外`ANT-ACE`提供了一定的调试支持,通过部分支持对于模型的部分加密支持和运行时校验为解决加密之后程序推理准确率下降的问题。
接下来三个报告都是关于如何将人工智能技术同编译技术解决起来。计算所冯晓兵老师的报告《人工智能编译领域的应用探索》介绍了大模型同编译后端的两个结合方向1使用大模型生成编译器的后端代码2使用大模型替换编译器的后端直接利用大模型生成汇编代码。华为毕昇编译器架构师魏伟的报告《AI for Compiler的技术探索和应用实践》则是介绍了毕昇编译器的自动调优器`Autotuner`,这个一个自动寻找最优化的编译参数组合工具。复旦大学张为华老师的报告《基于学习的编译优化技术》也是一个类似的工作,利用机器学习技术挖掘已有的编译系统中存在的相关知识来指导新的编译优化。
最后一个报告则是字节公司郑思泽研究员的《计算通信融合中的编译器设计》,该报告主要聚焦于如何实现在深度学习算子层的计算通信融合,这个报告主要由搞`MLIR`的同学听,我就摸鱼了。
### 智能终端操作系统OpenHarmony前沿研究
虽然名字叫作OpenHarmony但是感觉内容实际上和鸿蒙系统没有什么太大的关系。
第一个报告是软件所武延军老师的《万物智联时代基础软件如何驯服碎片化》。报告的标题非常的高大上但是实际上就讲了两件事情1RISCV架构或者说RISCV这个可扩展的思想是解决架构碎片化的思路2`openEular`系统可以作为系统软件适配的一个基线操作系统。总结一下,这其实就是一个广告,希望大家做基础软件的都来和大家一起做。
第二个报告是南京大学冯新宇老师的《基于仓颉语言的嵌入式DSL开发》同时冯新宇老师也是仓颉语言的首席架构师。冯老师的这个报告主要聚焦于仓颉语言提供的嵌入式DSL能力而嵌入式DSL这一设计范式已经在前端开发中展现了不俗的潜力。报告中介绍了嵌入式DSL出现的背景仓颉中为了提供嵌入式DSL而引入的语法糖、仓颉提供的嵌入式DSL工具箱等。虽然仓颉语言是一个主要面向上层应用开发的语言但是仓颉中丰富的DSL能力还是给异构编程模型的设计提供了不少的启发。而且目前在各种深度学习编译器中DSL的应用也非常广泛例如`triton`
![image-20241102212536635](./cncc-2024/image-20241102212536635.webp)
第三个报告是在存算一体的芯片上做数据库的加速第四个报告是OpenHarmony上`ArkTS`程序的静态分析,都没怎么听。
最后一个又是上交杜冬冬老师的报告,《面向下一代智能终端操作系统的渲染服务研究与挑战》。这是一个我感觉还挺有趣的报告,报告中介绍的主要背景是随着终端设备上屏幕刷新率的提高和操作系统动画变得更加精致复杂,用户会发现终端系统上的显示卡顿越来越多、越明显。这是因为目前的终端显示刷新机制是同步的,显示屏会按照当前刷新的频率从操纵系统中读取下一帧的画面,但是操作系统面对这越来越短的刷新时延和越来越复杂的动画常常不能按时把下一帧的画面渲染好。于是我们的杜冬冬老师就提出了一种动态、异步的渲染机制,考虑到系统中显示动画的时间还是占少部分的,于是就可以借用这些系统不繁忙的时间预先渲染(削峰填谷)。但是这种方式需要预知到系统后面会显示的内容,这使得这套技术只能在确定性的场景和部分简单交互场景下使用。
> 这里插入一个杜冬冬老师的八卦杜老师改过一次名字之前的名字是杜东Dong Du在查找论文的时候使用后面的名字会更好一些在[IPADS](https://ipads.se.sjtu.edu.cn/zh/members/)和[dblp](https://dblp.org/pid/48/331-3.html)上面都还没有改过来)。
## 其他
首先我要锐评一下浙江省金华市东阳市横店镇。横店镇感觉完全没有为一个旅游目的地做过准备,虽然说镇子上面的酒店还是挺多的,但是不管是吃的还是玩的感觉都非常少。而且镇上的交通简直就是一坨,尤其是我们从酒店到会议举办地圆明新园的一段路,完全被大货车摧残的不成样子,在上面坐车堪比过山车。
然后我要锐评一下会议的举办地横店圆明新园。在去之前听说这里是1:1复刻了被八国联军烧毁的圆明园结果去了才发现圆明新园分成春苑、夏苑和秋苑其中春苑是复刻的圆明园但是会议的举办地是在夏苑和秋苑感觉有点的被诈骗了。夏苑里面只复刻了圆明园长春园的部分景观比如海岳开襟、谐奇趣和大水法等而且还增设了英、法、美、俄、日、德、意和奥等国的特色建筑而会议就主要在这些特色建筑中进行属实感觉有点奇怪了。
最后我要锐评一下CNCC会议。名义上看这个会议有涵盖数十个方向的130余场论坛上万名注册参会者的大型会议但是这个会议却选在了一个看上去基本上不适合召开大型会议的横店镇圆明新园。同时会议进行的非常寒酸中午的午餐是横店提供给剧组的盒饭在主会场发给我们之后只能自己端着吃下午的茶歇更是少的可怜除了第三天有好哥们分了我一块蛋挞三天的茶歇我愣是一点都没见到有可能是第三天的人最少提高了我获得茶歇的概率

BIN
source/posts/cncc-2024/image-20241102211959206.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/cncc-2024/image-20241102212355390.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/cncc-2024/image-20241102212536635.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/cncc-2024/image-20241102212738598.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,390 @@
---
title: 编译MediaPipe框架
date: 2022-11-11T22:20:25.0000000
tags:
- C/C++
- 技术笔记
---
编译MediaPipe框架。
<!--more-->
最近开始研究自己的大创项目,一个关于动作捕捉的小玩意儿,第一步就是~~抄袭开源代码~~借鉴他人优秀成果。在众多的项目中我看上了这个Google开源的优秀框架先把这个项目在本地上跑起来再说。这篇文章就记录了我编译这个框架的过程。
> 在我写完这篇文章之后,我就从`WSL`润到了`Arch Linux`,然而我还是有编译`MediaPipe`的需求,所以这篇文章就增加了`Arch Linux`下编译`MediaPipe`的过程。
首先是在`Arch Linux`下编译的过程。
## 编译环境概述
使用`Arch Linux`,需要注意的是`Arch Linux`采用滚动更新,目前的安装方法可能在不久的将来就不适用了。
## 编译
### 环境准备
首先安装`bazelisk`,由于我安装了`pnpm`,直接使用`pnpm install -g @bazel/bazelisk`安装。
然后使用`pacman`安装:
```shell
sudo pacman -S opencv ffmpeg jdk-openjdk git
```
还有编译的过程中可能会涉及到`python`中的`numpy`软件包,我由于已经安装了`conda`来管理`python`环境,于是就采用`conda install numpy``base`环境中安装。
克隆`MediaPipe`仓库:
```shell
git clone https://github.com/google/mediapipe.git
```
由于`pacman`仓库中安装的`opencv`版本是最新的`opencv4`,我们需要修改`MediaPipe`中的配置文件来适配,修改`third_party/opencv_linux.BUILD`
```python
# Description:
# OpenCV libraries for video/image processing on Linux
licenses(["notice"]) # BSD license
exports_files(["LICENSE"])
# The following build rule assumes that OpenCV is installed by
# 'apt-get install libopencv-core-dev libopencv-highgui-dev \'
# ' libopencv-calib3d-dev libopencv-features2d-dev \'
# ' libopencv-imgproc-dev libopencv-video-dev'
# on Debian Buster/Ubuntu 18.04.
# If you install OpenCV separately, please modify the build rule accordingly.
cc_library(
name = "opencv",
hdrs = glob([
# For OpenCV 4.x
#"include/aarch64-linux-gnu/opencv4/opencv2/cvconfig.h",
#"include/arm-linux-gnueabihf/opencv4/opencv2/cvconfig.h",
#"include/x86_64-linux-gnu/opencv4/opencv2/cvconfig.h",
# 将下面这行取消注释
"include/opencv4/opencv2/**/*.h*",
]),
includes = [
# For OpenCV 4.x
#"include/aarch64-linux-gnu/opencv4/",
#"include/arm-linux-gnueabihf/opencv4/",
#"include/x86_64-linux-gnu/opencv4/",
# 将下面这行取消注释
"include/opencv4/",
],
linkopts = [
"-l:libopencv_core.so",
"-l:libopencv_calib3d.so",
"-l:libopencv_features2d.so",
"-l:libopencv_highgui.so",
"-l:libopencv_imgcodecs.so",
"-l:libopencv_imgproc.so",
"-l:libopencv_video.so",
"-l:libopencv_videoio.so",
],
visibility = ["//visibility:public"],
)
```
### 编译
跑一下实例中的`Hello, World`
首先设置一个环境变量:
```shell
$ export GLOG_logtostderr=1
```
然后一把梭哈:
```shell
bazel run --define MEDIAPIPE_DISABLE_GPU=1 \
mediapipe/examples/desktop/hello_world:hello_world
```
在第一次编译的时候会下载大量的依赖文件,如果遇到网络错误可以多试几次,~~我试了三次才完成~~。
```shell
Starting local Bazel server and connecting to it...
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'com_google_absl' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'com_google_benchmark' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'flatbuffers' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'pybind11_bazel' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'com_googlesource_code_re2' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'com_google_protobuf' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'com_google_googletest' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'com_github_gflags_gflags' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'zlib' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'rules_python' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'build_bazel_rules_apple' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'build_bazel_rules_swift' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'build_bazel_apple_support' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'xctestrunner' because it already exists.
DEBUG: /home/ricardo/.cache/bazel/_bazel_ricardo/9d4c3ea39592a9aa5075148d2a9caf3e/external/org_tensorflow/third_party/repo.bzl:132:14:
Warning: skipping import of repository 'pybind11' because it already exists.
WARNING: /home/ricardo/Documents/code/cpp/mediapipe/mediapipe/framework/tool/BUILD:184:24: in cc_library rule //mediapipe/framework/tool:field_data_cc_proto: target '//mediapipe/framework/tool:field_data_cc_proto' depends on deprecated target '@com_google_protobuf//:cc_wkt_protos': Only for backward compatibility. Do not use.
WARNING: /home/ricardo/Documents/code/cpp/mediapipe/mediapipe/framework/BUILD:54:24: in cc_library rule //mediapipe/framework:calculator_cc_proto: target '//mediapipe/framework:calculator_cc_proto' depends on deprecated target '@com_google_protobuf//:cc_wkt_protos': Only for backward compatibility. Do not use.
INFO: Analyzed target //mediapipe/examples/desktop/hello_world:hello_world (84 packages loaded, 1747 targets configured).
INFO: Found 1 target...
Target //mediapipe/examples/desktop/hello_world:hello_world up-to-date:
bazel-bin/mediapipe/examples/desktop/hello_world/hello_world
INFO: Elapsed time: 3.962s, Critical Path: 0.31s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
I20230115 20:26:29.880736 47247 hello_world.cc:57] Hello World!
I20230115 20:26:29.880805 47247 hello_world.cc:57] Hello World!
I20230115 20:26:29.880817 47247 hello_world.cc:57] Hello World!
I20230115 20:26:29.880888 47247 hello_world.cc:57] Hello World!
I20230115 20:26:29.880957 47247 hello_world.cc:57] Hello World!
I20230115 20:26:29.881028 47247 hello_world.cc:57] Hello World!
I20230115 20:26:29.881096 47247 hello_world.cc:57] Hello World!
I20230115 20:26:29.881157 47247 hello_world.cc:57] Hello World!
I20230115 20:26:29.881198 47247 hello_world.cc:57] Hello World!
I20230115 20:26:29.881268 47247 hello_world.cc:57] Hello World!
```
### 编译Pose解决方案的Android Archieve
我的需求有一个是在安卓手机上使用`MediaPipe``Pose`解决方案,需要编译出一个`AAR`安卓打包文件。
首先安装需要的依赖,`Android SDK``Android NDK`。这里使用`Android Studio`安装。
然后创建一个`bazel`编译的目标文件:在目录`mediapipe/examples/android/src/java/com/google/mediapipe/apps/`下创建一个文件夹`pose_tracking_aar`,在其中创建`BUILD`文件,写入:
```python
load("//mediapipe/java/com/google/mediapipe:mediapipe_aar.bzl", "mediapipe_aar")
mediapipe_aar(
name = "mediapipe_pose_tracking",
calculators = ["//mediapipe/graphs/pose_tracking:pose_tracking_gpu_deps"],
)
```
使用命令编译:
```shell
bazel build -c opt --strip=ALWAYS \
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
--fat_apk_cpu=arm64-v8a,armeabi-v7a \
--legacy_whole_archive=0 \
--features=-legacy_whole_archive \
--copt=-fvisibility=hidden \
--copt=-ffunction-sections \
--copt=-fdata-sections \
--copt=-fstack-protector \
--copt=-Oz \
--copt=-fomit-frame-pointer \
--copt=-DABSL_MIN_LOG_LEVEL=2 \
--linkopt=-Wl,--gc-sections,--strip-all \
//mediapipe/examples/android/src/java/com/google/mediapipe/apps/pose_tracking_aar:mediapipe_pose_tracking.aar
```
如果在编译的过程中提示缺失`dx.jar`这个文件而且你用的SDK版本还是高于31的那可能是SDK中缺失了这个文件可以将SDk降级到30就含有这个文件了。我使用的解决办法比较离奇我是将30版本的SDK文件中的这个文件软链接过来解决了这个问题。
![](compile-mediapipe/2023-01-15-22-05-41-Screenshot_20230115_220521.webp)
编译消耗的时间可能比较的长,耐心等待即可。
为了在手机上使用,我们还需要编译出`binarypb`文件从Google的服务器上下载`tflite`文件。
编译`binarypb`的过程比较的简单,编译目标在`mediapipe/graphs/pose_tracking`中,名称是`pose_tracking_gpu_binary_graph`,使用下列指令编译:
```shell
bazel build -c opt //mediapipe/graphs/pose_tracking:pose_tracking_gpu_binary_graph
```
> 在这里Google默认添加了一个`input side packet`打开人体遮罩,如果不需要这个效果,需要删除`mediapipe/graphs/pose_tracking/pose_tracking_gpu.pbtxt`文件中的以下内容:
>
> ```
> # Generates side packet to enable segmentation.
> node {
> calculator: "ConstantSidePacketCalculator"
> output_side_packet: "PACKET:enable_segmentation"
> node_options: {
> [type.googleapis.com/mediapipe.ConstantSidePacketCalculatorOptions]: {
> packet { bool_value: true }
> }
> }
> }
> ```
然后还需要从服务器上下载`tflite`文件,`Pose Tracking`这个解决方案需要两个`tflite`文件,第一个是[pose_detection.tflite](https://storage.googleapis.com/mediapipe-assets/pose_detection.tflite),第二个文件则有三个不同的选择,分别对于解决方案中提供的三个质量版本:
![](compile-mediapipe/2023-01-19-20-20-40-Screenshot_20230119_202008.webp)
下载地址是[pose_landmark_full.tflite](https://storage.googleapis.com/mediapipe-assets/pose_landmark_full.tflite)[pose_landmark_heavy.tflite](https://storage.googleapis.com/mediapipe-assets/pose_landmark_heavy.tflite)和[pose_landmark_lite.tflite](https://storage.googleapis.com/mediapipe-assets/pose_landmark_lite.tflite)。
> 下面是原来使用`WSL`编译的过程。
## 编译环境概述
我使用的基于WSL2的Ubuntu 22.04编译。主要是参考官方的[安装文档](https://google.github.io/mediapipe/getting_started/install.html#installing-on-debian-and-ubuntu)。
## 编译
### 环境准备
首先安装两个基础性的包:
```bash
sudo apt install build-essential git
```
然后安装MediaPipe的编译管理工具`bazel`,这里我是通过`npm`安装:
```bash
pnpm add -g @bazel/bazelisk
```
通过运行`bazel version`验证安装成功:
```bash
$ bazel version
Bazelisk version: v1.13.2
WARNING: Invoking Bazel in batch mode since it is not invoked from within a workspace (below a directory having a WORKSPACE file).
Extracting Bazel installation...
Build label: 5.3.2
Build target: bazel-out/k8-opt/bin/src/main/java/com/google/devtools/build/lib/bazel/BazelServer_deploy.jar
Build time: Wed Oct 19 18:22:12 2022 (1666203732)
Build timestamp: 1666203732
Build timestamp as int: 1666203732
```
安装`Miniconda`,再在环境中安装`numpy`。编译中依赖于`Python``numpy`,这里网上的资料汗牛充栋,我就不过多赘述。
在准备玩上面的环境之后,我们就可以用`git`将MediaPipe的源代码克隆下来了。
```bash
git clone https://github.com/google/mediapipe.git
```
### 安装Opencv和FFmpeg
我这里选择的是手动编译安装opencv安装的步骤参考官方的安装脚本但是脚本中的不少内容已经过时。
首先安装必要的依赖库和编译管理工具:
```bash
sudo apt install cmake ffmpeg libavformat-dev libdc1394-dev libgtk2.0-dev \
libjpeg-dev libpng-dev libswscale-dev libtbb2 libtbb-dev \
libtiff-dev
```
> 注意:官方脚本中要求安装`libdc1394-22-dev`这个包,但是按照这篇[回答](https://askubuntu.com/questions/1407580/unable-to-locate-package-libdc1394-22-dev),这个包已经被`libdc1392-dev`取代了。
在临时文件夹中创建一个文件专门用来编译:
```bash
cd /tmp
mkdir opencv
cd opencv/
```
使用`git`下载Opencv的源代码
```bash
git clone https://github.com/opencv/opencv_contrib.git
git clone https://github.com/opencv/opencv.git
```
在仓库中签出到指定的版本分支:
```bash
cd opencv
git checkout 3.4
cd ../opencv_contrib
git checkout 3.4
```
创建编译文件,使用指定的`cmake`参数生成编译文件:
```bash
cd ../opencv
mkdir release
cd release
cmake .. -DCMAKE_BUILD_TYPE=RELEASE -DCMAKE_INSTALL_PREFIX=/usr/local \
-DBUILD_TESTS=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_opencv_ts=OFF \
-DOPENCV_EXTRA_MODULES_PATH=/tmp/opencv/opencv_contrib/modules \
-DBUILD_opencv_aruco=OFF -DBUILD_opencv_bgsegm=OFF -DBUILD_opencv_bioinspired=OFF \
-DBUILD_opencv_ccalib=OFF -DBUILD_opencv_datasets=OFF -DBUILD_opencv_dnn=OFF \
-DBUILD_opencv_dnn_objdetect=OFF -DBUILD_opencv_dpm=OFF -DBUILD_opencv_face=OFF \
-DBUILD_opencv_fuzzy=OFF -DBUILD_opencv_hfs=OFF -DBUILD_opencv_img_hash=OFF \
-DBUILD_opencv_js=OFF -DBUILD_opencv_line_descriptor=OFF -DBUILD_opencv_phase_unwrapping=OFF \
-DBUILD_opencv_plot=OFF -DBUILD_opencv_quality=OFF -DBUILD_opencv_reg=OFF \
-DBUILD_opencv_rgbd=OFF -DBUILD_opencv_saliency=OFF -DBUILD_opencv_shape=OFF \
-DBUILD_opencv_structured_light=OFF -DBUILD_opencv_surface_matching=OFF \
-DBUILD_opencv_world=OFF -DBUILD_opencv_xobjdetect=OFF -DBUILD_opencv_xphoto=OFF \
-DCV_ENABLE_INTRINSICS=ON -DWITH_EIGEN=ON -DWITH_PTHREADS=ON -DWITH_PTHREADS_PF=ON \
-DWITH_JPEG=ON -DWITH_PNG=ON -DWITH_TIFF=ON
```
> 注意安装自己下载源代码的地址修改`-DOPENCV_EXTRA_MUDULES_PATH`的值
> 安装过程中还会下载一系列的依赖包,请注意自己的网络环境
使用`make`指令进行编译和安装
```bash
make -j 16
sudo make install
```
编辑链接器的配置:
```bash
sudo touch /etc/ld.so.conf.d/mp_opencv.conf
sudo bash -c "echo /usr/local/lib >> /etc/ld.so.conf.d/mp_opencv.conf"
sudo ldconfig -v
```
然后进行MediaPipe的目录用脚本进行配置文件的修改
```bash
./setup_opencv.sh config_only
```
## 运行首个例子:
```bash
export GLOG_logtostderr=1
bazel run --define MEDIAPIPE_DISABLE_GPU=1 mediapipe/examples/desktop/hello_world:hello_world
```
在等待一段时间的下载依赖和编译之后,我们可以看见:
```bash
I20221110 22:00:50.899885 14357 hello_world.cc:57] Hello World!
I20221110 22:00:50.899948 14357 hello_world.cc:57] Hello World!
I20221110 22:00:50.899955 14357 hello_world.cc:57] Hello World!
I20221110 22:00:50.899960 14357 hello_world.cc:57] Hello World!
I20221110 22:00:50.899962 14357 hello_world.cc:57] Hello World!
I20221110 22:00:50.899982 14357 hello_world.cc:57] Hello World!
I20221110 22:00:50.900000 14357 hello_world.cc:57] Hello World!
I20221110 22:00:50.900025 14357 hello_world.cc:57] Hello World!
I20221110 22:00:50.900030 14357 hello_world.cc:57] Hello World!
I20221110 22:00:50.900193 14357 hello_world.cc:57] Hello World!
```
如果出现了各种奇怪的报错,那可以执行这条命令重新安装依赖再编译试试:
```bash
bazel clean --expunge
```

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,77 @@
---
title: 计算机系统结构——流水线复习
date: 2024-06-12T20:27:25.0000000
tags:
- 计算机系统结构
- 学习资料
---
让指令的各个执行阶段依次进行运行是一个简单而自然的想法,但是这种方式执行速度慢、运行效率低。因此一个很自然的想法就是将指令重叠起来运行,让执行功能部件被充分的利用起来,这就是**流水线**。
流水线的表示方法有两种。
![image-20240612184855300](computer-architecture-pipeline/image-20240612184855300.webp)
第一种被称作**连接图**,清晰的表达出了流水线内部的逻辑关系。
![image-20240612184949777](computer-architecture-pipeline/image-20240612184949777.webp)
> 上图中给出了两个流水线中的概念:通过时间和排空时间。其中通过时间又被称作装入时间,是指第一个任务进入流水线到完成的事件;排空时间则相反,是最后一个任务通过流水线的时间。
第二种被称作**时空图**,通过在图中画出一个指令序列通过流水线的过程表现出流水线的时间关系。
在流水线中,每个功能部件之后需要存在一个寄存器,这个寄存器被称为流水寄存器,其作用为在流水线相邻的两端之间传递数据,并且把各段的处理工作相互隔离。
流水线有着多种分类。
按照等级分:
- 部件级,将处理机中的部件分段相互连接,也称作运算操作流水线。
- 处理机级,将指令的执行过程分解为若干个子过程,就是指令流水线。
- 系统级,把多个处理机串行连接,对同一数据流进行处理,称作宏流水线。
按照完成功能的多倍性:
- 单功能,流水线各段之间的连接固定,只能完成一种功能。
- 多功能,段之间的连接可以变化,不同的连接方式可以完成不同的功能。
其中多功能还能进一步分为:
- 静态流水线,同一时间内,多功能流水线的各段只能按照同一种功能的方式连接。
- 动态流水线,同一时间内,多功能流水线的各种可以按照不同的方式连接,执行不同的功能。
![image-20240612190426368](computer-architecture-pipeline/image-20240612190426368.webp)
按照流水线中是否存在反馈回路分类:
- 线性,串行连接,没有反馈回路,每个段只能流过一次。
- 非线性,存在反馈回路。
根据任务流入和流出的顺序是否相同:
- 顺序流水线
- 乱序流水线
流水线的性能指标一般由三个指标衡量:
- 吞吐率,单位时间流水线完成任务的数量,或者输出结果的数量。
- 加速比,同一任务,不使用流水线所使用时间与使用流水线所用时间比。
- 效率,流水线设备的利用率。
![image-20240612192700169](computer-architecture-pipeline/image-20240612192700169.webp)
在设计流水线的过程中存在若干问题。
1. 瓶颈问题。当流水线各段不均匀时,机器的时钟周期取决于瓶颈段的延迟时间,因此设计流水线时,应当使各段的时间相等。
2. 流水线的额外开销。由于流水寄存器的延迟和时钟偏移开销,流水线往往会增加单条指令的执行时间,当时钟周期过小时,流水已经没有意义。
3. 冲突问题。数据冲突、结构冲突和控制冲突。
一个典型的五段流水线MIPS流水线
![image-20240612193301372](computer-architecture-pipeline/image-20240612193301372.webp)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,133 @@
---
title: 日用Linux挑战 第0篇 初见Arch Linux
date: 2023-01-15T22:23:08.0000000
tags:
- Linux
- 杂谈
---
在将开发重心移到`WSL`上一年之后我最终还是决定完全抛弃Windows转向使用Linux作为我日常使用的主力系统。目前我已经使用Linux作为主力系统一个月了。
<!--more-->
## 开始之前
### 电脑
首先介绍一下用来安装Linux系统的电脑——荣耀的`Magicbook 14`,我今年秋天才购入的轻薄笔记本。
- CPUR7-6800H
- 内存DDR5 6400MHz 16GB
- 硬盘: 西部数据 SN550 1T
> 硬盘为我自行更换,原装的盘被我搞掉盘了(笑)
其他的像网卡啥的我就不一一介绍了,具体的硬件兼容性我后面会专门说明。
### 选择发行版
众所周知选择Linux最重要的一步就是选择一个适合自己的Linux发行版。在正式将Linux作为主力系统之前我使用的比较多的发行版是`Ubuntu`,在`WSL`、服务器和树莓派上我都是使用的它。
不过这次,我选择了`Arch`发行版。虽然这个发行版不像`Ubuntu`一类的发行版提供了开箱即用的使用体验,而且安装`Arch`的过程也很难称作简单,但是经过两年半~~小黑子露出了鸡脚~~的Linux练习之后我对于命令行界面的使用还是比较熟悉了。而且`Arch`还提供了其他一些非常吸引我的点:
- 默认的`Arch`安装是一个极简的操作系统,甚至没有图形化的界面,这就提供了一个非常大的优点——不会有任何预装的垃圾
- `Arch`采用滚动升级模型,尽全力让所有的软件包都保持在最新,而我恰好使用的是比较新的硬件,为了避免出现兼容性的问题,我希望能够使用最新的`Linux`内核和软件~~虽然我实际测试Ubuntu等系统也能正常使用~~
- `Arch`还提供了一个由用户维护的软件安装源——`AUR`,一些没有进入官方软件源的软件可以方便的在这里一键安装,非常的好用,非常的方便
- `Arch`还提供了一份非常详尽的`wiki`文档
### 选择桌面环境
我在Linux系统中长时间使用过的桌面环境有`GNOME``KDE`两种。
我首先安装的是`gnome`这个桌面环境,在使用的过程中常常会遇到桌面卡死的情况,切换到其他的`tty`重启`gdm.service`之后就能恢复,在尝试修复无果之后我就切换到了`KDE`桌面环境,目前用上还算满意。
## Hello, Linux!
### 安装Arch
按照`wiki`上的安装教程安装的过程还算的上是比较的顺畅大概90分钟的时间完成了我的第一次`Arch`安装。
不过在安装的过程中,有些问题需要注意:
- 虽然官方Wiki有着中文翻译的版本但是在参考的时候需要注意查看同英语原文之间有没有滞后的内容`Arch`作为一个滚动更新的版本,安装的方法也常常发生变化。
- 在我下载的安装镜像中的`pacman mirrorlist`文件中并没有大陆常用镜像服务器的地址,需要手动添加。我添加了清华大学和浙江大学两个镜像站的地址。
- 如果没有有线的网络链接,记得在系统中安装联网所需要的程序,比如`NetworkManager`啥的,否则你安装完成之后发现没有办法联网,只能再次从安装镜像启动再安装相关的软件。
### 安装桌面环境
在安装桌面环境之前,先安装显卡的驱动程序。
参考文档,需要安装`mesa``lib32-mesa``vulkan-radeon`三个软件包,其中`lib32-mesa`软件包是对32位程序提供支持的软件包为了安装这个软件包需要打开`multilib`这个仓库,取消`/etc/pacman.conf`这个文件中对于`[multilib]`部分的注释来实现这个功能。
```shell
sudo pacman -S mesa lib32-mesa vulkan-radeon
```
由于使用的是`KDE`桌面环境,这个桌面环境对于`wayland`显示服务器的支持还不算太好,于是首先安装`xorg`显示服务器和显示驱动程序。
```shell
sudo pacman -S xorg-server
```
然后安装桌面和`sddm`显示管理器
```shell
sudo pacman -S plasma sddm
sudo systemctl enable sddm.service
```
重启之后图形界面就可以正常显示了。
#### 桌面环境的美化
都在使用`Arch`了,不折腾一下桌面环境的美化是不可能的。
我目前实现的效果大概长这样:
![](2023-01-12-13-28-38-Screenshot_20230112_132829.webp)
颇有一种`Windows``MacOS`杂交的风格。
使用的主题地址为[GitHub - vinceliuice/WhiteSur-kde: MacOS big sur theme for kde plasma](https://github.com/vinceliuice/WhiteSur-kde),添加了一个`Windows 10`风格的开始菜单插件[GitHub - Zren/plasma-applet-tiledmenu](https://github.com/Zren/plasma-applet-tiledmenu),字体是直接从`Windows`下面复制过来的`Microsoft YaHei UI`
> 顺便提一下我发现安装kde主题最快的方式是找到主题的Github仓库`clone`之后安装。这种方法比去折腾那个不好用的主题商店快一万倍。
目前在美观上还存在的缺憾是登录界面和锁屏界面我还没有去折腾,还是默认的样子。
### 安装常用软件
先上一张`shell`的系统概览截图:
![](2023-01-12-13-36-45-Screenshot_20230112_133628.webp)
终端模拟器直接使用的`konsole`,目前没有进行改动。
系统中使用的输入法是`ibus`框架下的`rime`输入法,词库不太智能。浏览器使用的`edge`几乎完全复制了我在Windows下的体验邮件客户端我使用的`ThunderBird`,不过没法后台通知;`Markdown`写作使用的是`MarkText`,几乎可以取代`Typora`腾讯在2022年的12月30日发布了新版的`QQ`也算是补上了Linux上一个比较大的短板不过目前的支持还是比较差甚至连收发文件都不支持。至于`office`办公软件,我的需求不是很明显,采用`onedrive``Microsoft 365`在浏览器端基本上解决了。编程方面,`VSCode``Jetbrains`这两个我主力的IDE都能在Linux下正常的工作。在游戏方面上我最近玩的`Hearts of Iron 4`有着Linux原生的版本运行流畅`原神`也在利用一些小手段和`wine`之后,比较流畅的运行起来了,至于`Steam`提供的`Pronton`兼容层,还没怎么用过。
### 发现的兼容性问题
显然目前的Linux也不可能做到尽善尽美。
- 在睡眠之后,电脑没有办法正常的播放声音,应该是内核中对于新硬件的支持问题。
- 指纹识别失效了。`fprint`库上还没有提供对我这个硬件的支持。~~也有可能永远不会提供~~
- 笔记本方面的支持还是有所欠缺,也有可能是因为我的调教还不到位。出现过几次合上盖子之后没有办法唤醒的问题,最后只能强制关机后重启。
## 日常使用一个月
作为一个程序员,`Arch Linux`确实非常适合我使用。还记得在折腾`WSL`的时候,`jetbrains`的IDE写代码的时候bug非常多只有`VSCode`的支持稍微好一点,但是作为一个`IDE`的功能又不是很强大。`WSL`还有着一堆兼容性的问题,我还记得在`WSL`脱离`preview`标志之后,我有好几次一启动`WSL`就蓝屏的经历,属实难忘。
不过使用Linux还是有很多不方便的地方尤其是在国内办公软件的兼容性问题上。虽然很多国产软件都提供了Linux的版本不过一般都是统信UOS或者是麒麟Kylin版本的为了在Arch Linux上运行还需要自己研究一下。在这里我要点名批评"Q*"软件,其从官网上下载的`AppImage`格式的程序文件中还有错误,内附的`qq.desktop`文件中的图片路径竟然是写死`/opt/...`。我的评价是做程序的人还是用点心罢,至少打包完成了还是自己测试一遍罢。
平心而论目前的Linux已经可以成为一个日常使用的操作系统了。虽然仍然不能完全摆脱命令行界面普通用户按照`Windows`下的使用经验来使用还存在一定的困难,但是如果你是计算机相关从事人员而且对游戏没有太大的依赖,`Linux`是一个不错的选择。
不出意外的话,本文将是一个系列文的第一篇。我将定期更新这个系列,记录我在学习和工作中使用`Linux`的体验和感受。

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,78 @@
---
title: 日用Linux挑战 第1篇 问题与挑战
tags:
- Linux
- 杂谈
date: 2023-03-08 22:37:29
---
从去年12月底正式切换到`Linux`开始算起,我日常使用`Linux`已经过去了2个月的时间。在本系列的上一篇文章——[日用Linux挑战 第0篇 - Ricardo的博客](https://rrricardo.top/blog/2023/01/15/daily-linux-0/)中,我讲述了我配置自己的`Arch Linux`的过程,还小小的赞扬了一波`Linux`在近些年来取得的进展。但是在这篇文章中,我将重点指出日常使用过程中遇到的问题和困难。
<!--more-->
## 尝试迁移到Wayland
众所周知,`X11`已经是一个落后和过时的显示协议,目前更新的显示协议`Wayland`正在尝试干掉它,但是由于`X11`长期使用带来的稳定性这种工作并不是那么容易。现在是2023年`Wayland`的工作进展到了何种程度?
我使用`KDE`作为我的桌面环境。`KDE`已经有了基础的`Wayland`支持,在启动界面选择`Wayland`会话就可以正常进入。
虽然在切换到`Wayland`会话之后,除了`KDE` 自带的应用程序,日常使用的大部分应用程序还是运行在`xwayland`之下,例如`edge``firefox`浏览器,`VSCode`等。
`/etc/environment`中设置`MOZ_ENABLE_WAYLAND=1`可以`firefox`切换到`Wayland`会话下,运行正常。
`~/.config/microsoft-edge-stable-flags.conf`添加`--ozone-platform-hint=auto``edge`切换到`Wayland`下,运行正常。
然而存在的问题也不少。
- `sddm`窗口管理器在关机的时候没法正常的关闭导致每次关机都需要等待1分30秒。
而且这是一个`sddm`的问题,从[相关Issue](https://github.com/sddm/sddm/issues/1476)上看,这个问题已经在`master`分支被修复了,只是在相关发行版中还没有修复。
采用切换到`sddm-git`的方式修复了这个问题。
> 而且在`archlinuxcn`仓库里面还有这个包,赞美`archlinuxcn`。
- `ibus-rime`输入法在`Wayland`下的适配也是非常的糟糕,甚至是一种不稳定的糟糕,每次出现的问题都不一样,比如现在的问题就是在`Edge`浏览器下无法使用。
在更换使用`fcitx-rime`框架之后稍微好了一点,至少能用了(
- 部分网站的登录信息丢失。
- 使用`BiliBili`全屏播放视频的时候,只要鼠标移入全屏的范围,画面就会黑屏。可能和我使用双显示器有一定的关系。
- 使用`125%`的整体缩放比例的时候,字体处于一种模糊的状态,看上去很不舒服。
简单的说,我不认为现在`Linux`已经准备好切换到`Wayland`下了。
> 听说最新的`Ubuntu 22.04`已经默认使用`Wayland`作为显示协议了,等我有了其他的电脑可以试一试,看看商业公司的加入能不能带来一点转机。
## 使用中发现的问题
### 双屏使用的问题
最近入手了一块2K 75Hz的VA显示屏本来打算美滋滋的使用双屏高效率的~~摸鱼~~学习,结果发现问题实在是非常的一言难尽。
- 我的两块屏幕的分辨率不是完全一致的笔记本自带的屏幕是3:2的2160x1440而新买的显示屏是16:9的2560x1440。这就导致两块屏幕的纵向分辨率是一致的而横向分辨率是不同这点似乎迷惑了`KDE`的相关处理程序,导致两块屏幕各有一个部分显示在另外一块屏幕上~~虽然有点抽象,但是我相信你能够想象出来,如果我再次遇到我一定会补一张图的~~。虽然这个问题通过重新设置`Display configuration`就可以解决,但是真的很好笑。
- 部分程序也会因为上面的分辨率不同而导致部分意料之外的情况。比如原神,不如画面整体被横向压缩而出现一大堆椭圆,就是画面的两端有黑边,导致现在我玩原神都是合上笔记本显示屏玩。
> 看来以后想在`Linux`想使用双屏需要购买两块分辨率完全一致的显示屏。
- 程序在两块屏幕上的显示完全是混乱的。比如我打开`IDEA`,启动界面显示在一块屏幕上,选择项目之后的编辑界面又会出现在另外一块屏幕上。
### Wine`键盘按键不停重复的问题
在某次`sudo pacman -Syu`之后,我遇到一个奇怪的现象——在打原神的时候,如果较长时间的按下某一个按键,那么那个按键就会不停的重复,就像没有弹起一样。例如我按下`W`键较长时间再松开,人物仍然会前进,就像我没有松开一样。
当时我的解决办法是在系统设置里临时关闭了`When a key is held`选项再进行游戏。通过查看`pacman`的更新日志,我以为是`plasma`更新的问题。
在之后的某天中,为了折腾`Wayland`,我把输入法从`ibus`框架切换到了`fcitx5`框架,惊喜的发现这个问题消失了。
那么新的问题出现了:这个问题到底是为啥出现捏?不过本着多一事不如少一事的精神,我选择能跑就行,管它为什么。
## 赞美Kde Connect
作为一个大学牲,在自己的不同设备之间频繁的传送文件自然是家常便饭。在进入`Linux`之前,我几乎都是使用`QQ`在我的手机、iPad和笔记本电脑之间共享文件属于是究极折磨。入境大力拥抱`Linux``KDE`,没想到还有意外收获——`KDE connect`。在手机和iPad和笔记本电脑上安装和互相配对之后不同的设备之间就可以方便的发送和接受文件了。妈妈再也不同担心我传送文件的难题了。
好了下次攒够一波问题和经历在更新新一期Linux日用挑战。

View File

@@ -0,0 +1,91 @@
---
title: 日用Linux挑战 第2篇 Wayland
date: 2023-07-23T11:44:34.0000000
tags:
- 杂谈
- Linux
---
使用`Linux`6个月我成功戒掉了原神。
<!--more-->
## 使用Wayland开启桌面环境
在上一篇[文章](https://rrricardo.top/blog/2023/03/08/daily-linux-1/)我尝试在`kde`桌面环境下使用`wayland`显示协议,当时的尝试虽然失败了,却为我这次迁移到`wayland`打下了良好的基础,例如我将输入法从迁移到`fcitx5`
最近恰好被平铺式的窗口管理器种草又在B站上看见一个动画绚丽的`wayland`合成器——[Hyprland](https://hyprland.org/),当即脑袋一热,就把`kde`干掉,装上了`hyprland`
![img](df4211f6be2724b3b4725f7ce5a4078818844857.avif)
安装`hyprland`的过程非常舒适,`hyprland`被打包为一个单独的二进制文件,使用`pacman`安装之后直接在`tty`下执行:
```bash
Hyprland
```
就可以打开一个干净到极致的桌面环境——没有图标,状态栏等等传统桌面应该有的一切,只有一张简单的壁纸。然后我就发现我完全不会使用这个桌面,虽然鼠标还是如预期一般出现了,但是没有任何用处,`hyprland`的一切都需要使用键盘启动。在对着`hyprland`8个大字发愣5秒钟之后我便屁滚尿流的滚回了`tty`。虽然文档中说明了在默认的配置文件中使用`win+q`的组合键打开终端,但是默认的终端应用程序是`kitty`,然而这个冷门的终端程序在我的电脑上自然是没有的(虽然我在使用过后觉得这个中断程序秒杀我之前用过的全部终端)。看到简陋但是熟悉的`tty`仿佛见到了亲人,我一波`sudo pacman -S kitty`,一边看着进度条飞涨一边感谢`archlinux`。迅速按下`ctrl+shift+F1`,回到忠诚的壁纸画面,不过这次,轻轻按下`win+q`,一个终端窗口如宿命一般出现!
> 上面因为作者刚考完期末神智有点不正常,还请~~根本不存在的~~读者海涵。
`Linux`下拥有了终端就好办了。首先装上`App Launcher`——没有这玩意儿我似乎就只能从终端里启动应用程序,显然是十二分的不方便。在一番比较之后,我在官方文档中选择了`fuzzel`,看上去挺好看的。
> 本来这里想截一张`fuzzel`的图,但是似乎他们的主页挂掉了(
安装了`App Launcher`之后,至少可以打开浏览器,愉快的复制粘贴安装了。剩下的安装过程就按下不表,基本上按照官方文档的`Useful Utilities`进行,在中间选择自己需要的软件的进行安装。然后便是根据配置文件的说明对于外观和使用快捷键进行调整,以及配置壁纸软件`hyprpaper`和状态栏组件`waybar`。具体的配置文件我都放在了自建的`git`服务器上,可以在[这里](https://git.rrricardo.top/jackfiled/dot-config)查看。
### Wayland软件兼容性
跳槽到了新的`wayland`显示协议,最关心的自然是各种软件是否能在`wayland`下正常的工作。
首先是各种浏览器。在我安装的这段时间里2023年5月`microsoft edge`浏览器是基本不能在`wayland`模式下工作,一进入全屏模式就会自动崩溃退出,这对于日常电脑刷视频的我来说简直不能忍受。幸好,`firefox`浏览器在`wayland`模式下工作正常,不愧是和`Linux`关系最好的浏览器。再通过一系列的测试,`chromium`系列的浏览器在`wayland`下的工作状态都不太好,如果需要在`wayland`环境下使用`chromium`系列的浏览器,建议还是运行在`xwayland`模式下。
然后是各种开发工具。我日常使用的`jetbrains`IDE和`VSCode`都工作正常,虽然是工作在`xwayland`模式下。
各种在学习过程中遇到的工具软件基本上都工作运行良好。当然因为没有设置缩放的问题而导致字体都很小。因为如果在配置文件中设置缩放之后会导致字体发虚。下面的截图就是我将我的2K显示屏设置为150%缩放的效果,~~虽然在截图中的效果不明显~~。目前在常用软件中唯一让我十分不满意的软件是`wps`,使用体验完全无法和`offices`相提并论,目前我正在研究使用`wine`运行`offices`,如果成功了就再水一篇博客庆祝一下。
![image-20230702205919301](image-20230702205919301.webp)
> 最新的进展是使用`wine`没法安装学校提供的`office 2021`,同时我又不愿意使用古老的`office`版本,但是我发现一个称作`onlyoffice`的第三方软件蛮好用的,等我试用一段时间再说。
>
> 但是`onlyoffice`的最新版本和`wlroots`合成器似乎有点八字不合,我现在是回退到`7.2.1`版本正常使用,详情见这个[issue](https://github.com/ONLYOFFICE/DesktopEditors/issues/1208)
最后就是各种游戏的兼容性了。原神完蛋了。虽然这和`wayland`关系并不是很大,米哈游自己也罪大恶极,但是我换成`wayland`之后的游戏流畅度下降明显,只能眨眼补帧,在硬顶了几周之后我绷不住退游了。目前还不知道是由于原神本身一坨大便的优化还是`wayland`导致的性能下降。而我在`steam`上的各种游戏工作也不是很正常,尤其是各种需要全屏的游戏,比如`CS:GO`,可能是由于我使用外接屏幕的问题。但是`GalGame`类的游戏就工作正常。
总的来说,这次切换到`hyprland`的使用体验还是非常不错的,平铺式窗口管理器也是提高生产力的利器。
### `Hyprland`仍不完善
有得必有失,`hyprland`乃至于`wayland`目前作为一个桌面环境最大的问题就是相关生态仍不完善。这里的生态甚至都不是指对于`wayland`支持之类的东西,而是像系统设置、锁屏界面、状态栏之类的东西。虽然这样说可能有点对于`hyprland`的要求过高了,但是现在这种东拼西凑的构成一个桌面环境给人的体验不是很好,譬如使用`waybar`作为状态栏,`swaylock`作为锁屏界面,`kwallet`作为密码存储器。其中不少软件还都处于一个十分简陋的阶段,例如`swaylock`仅显示一张图片作为界面,甚至连个输入密码的界面都没有。
`hyprland`对于弹出窗口的支持不是很好,尽管这就是平铺式窗口管理器的设计思想。但是在像`idea`之类的编程工具中,弹出窗口是非常常用的功能,不能正常的显示弹出窗口或者显示的位置不对会导致严重的生产力下降。经过测试,当只使用一个显示屏是,可以正常的进行弹出窗口的显示,但是当链接多个显示器时,弹出窗口的位置就变得奇怪起来。
## `amdgpu`导致的相关问题
从我在这台电脑上安装`Linux`之后,便一直会出现图形界面卡死的问题。具体表现为图形界面停止响应,但是通过`ssh`等仍能正常远程链接,大小写锁定正常,使用`Ctrl+Alt+F3`组合键切换到其他`tty`的方式有一定的概率可以从卡死中恢复,但是也存在一定的可能性只能通过强制重启解决。
通过分析日志中的错误信息,大致锁定一下几个`issue`可能和这个问题有关:
- [random amdgpu hangs](https://gitlab.freedesktop.org/drm/amd/-/issues/2443)
- [flip_done timeout](https://gitlab.freedesktop.org/drm/amd/-/issues/2006)
- [ring sdma0 timeout](https://gitlab.freedesktop.org/drm/amd/-/issues/2220)
同时在`raddit``archlinux forum`上都存在大量的讨论均是怀疑为内核的问题。总结一下这个问题似乎和较新版本的内核和AMD的新`rdna2`显卡有关,同时是否链接外接显示器也有着不同的故障表现。而且,随着`linux kernel`不断的升级,报错信息似乎也在变化,~~虽然因为我的系统日志被自动覆盖,不能提供详尽的日志信息~~。
在某一篇讨论中很抱歉我忘记了具体页面有人指出这可能和内核对于CPU电压的调控有关在切换到AMD新发布的CPU频率调节驱动`amd p-state`之后,这个问题就不再出现了。查询[wiki](https://wiki.archlinux.org/title/CPU_frequency_scaling)可以发现,这个驱动目前并没有默认驱动,而是需要通过添加内核参数的方式启动。我使用`grub`作为启动程序,在配置文件`/etc/default/grub``GRUB_CMDLINE_LINUX_DEFAULT=`末尾添加新的启动参数`amd_pstate=active`,重启之后查看当前驱动:
```
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver
amd-pstate-epp
```
目前我已经启动这个新的驱动程序一周时间了,上述问题没有在出现过。

Binary file not shown.

BIN
source/posts/daily-linux-2/image-20230702205919301.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,98 @@
---
title: 日用Linux挑战 第3篇 放弃Wayland
date: 2023-09-04T14:47:46.0000000
tags:
- 杂谈
- Linux
---
成也开源,败也开源。
<!--more-->
## 放弃 `Wayland`
### 转向`labwc`
在使用`Hyprland`这个平铺式窗口管理器大概三个月之后,我开始怀疑自己是否真的需要一个平铺式的窗口管理器。
作为一个主打“平铺”式管理的桌面管理器,我却很少同时在一块屏幕上显示多个应用,都是将应用全屏之后在显示,再搭配`Meta+1\2\3`切换不同的`workspace`实现不同应用切换的功能。这让我开始思考,也许,我根本不适合使用一个“平铺式”的窗口管理器。其次,我常用的`IDE`——`IDEA`系列让我遇到了一个极为恶性的`bug`:代码重构的弹出式窗口需要很长的一段时间才能显示出来,其时间甚至足够我去喝口水上个厕所。如果我在上班,那么我肯定不会放弃这个显而易见的摸鱼机会,可惜不是。这个`bug`虽然不致命但是过于恶心,毕竟很好的重构思路被打断真的很想先把鼠标扔出去再把电脑砸了。
然后我就开始研究工作在`wayland`显示协议之下的堆叠式窗口管理器。首先,我惊喜的发现,目前`Linux`下的两大主流的桌面环境都开始默认使用`wayland`显示协议,~~虽然我并不打算将这两个臃肿的家伙请回来~~。打开我们伟大的`Arch Linux Wiki`,打开`wayland`页面,这里已经贴心的为我们整理好了使用`wayland`显示协议的堆叠式窗口管理器:
- `enlightenment`:进入官网一看,对于`wayland`的支持还是“实验性”阶段pass
- `hikari`:针对`FreeBSD`开发pass
- `KWin``KDE`使用的窗口管理器pass
- `Liri Shell`不知为何被我华丽无视pass
- `labwc`:看上去还行,一会试试
- `mutter``gnome`使用的窗口管理器pass
- `wayfire``aur`里面的官方包都不能过编译,这种粪软件还是算了吧
- `weston``wayland`示例合成器总给人一种怪怪的感觉pass
- `wio`:官网连接被标记为`dead-link`pass
首先尝试`labwc`,虽然这个软件在`Github`上只有800多颗星不过作为一个小而精的窗口管理器这个哥们还是比较完美的适配了我的需求虽然遇到诸多的小问题
- 没有提供在启动窗口管理器时设置环境变量的方法,导致只能修改`desktop`文件实现这个功能。
- 设置触控板的自然滚动失败
而且作为基于`wlroots`合成器开发的窗口管理器,可以完美继承我在`Hyprland`下的不少设置,像`waybar``hyprpaper`等等。
### 放弃
从今年五月份被`Hyprland`种草,切换到`Wayland`显示协议时算起,我已经使用了三个月的`Wayland`显示协议。刚刚切换到`labwc`使用没超过一周,我就遇到了在`wayland`下的最大缺陷:`matlab`没法正常的使用——虽然软件主界面还可以正常的打开,但是所有的子窗口都没办法打开。恰好那两天我必须要用`matlab`完成一个作业,我直接一波`sudo pacman -S plasma`润回了`KDE`
虽然这个故事听着可能有点突然,但确实就这么发生了,写作这篇博客时使用的桌面环境已经是`KDE`了。
下面就简单讲讲这次使用`KDE`的美化思路。这次使用`KDE`几乎完全继承了之前的[美化思路](https://rrricardo.top/blog/2023/01/15/daily-linux-0/),其中主要的变化为:
- 使用`kitty`作为终端模拟器
- 使用类似于`Windows`下的任务栏显示模式,而不是只显示图标
- 继承了部分在`Hyprland`下的快捷键
- `Meta+F`全屏应用
- `Meta+W`关闭应用
![](Screenshot_20230904_144149.webp)
### Fuck You NVIDIA
`matlab`还不是给予最后一击的东西。当我搞到一台使用NVIDIA显卡的笔记本时真正的噩梦才刚刚开始。首先是驱动作为一个被`FUCK`的公司NIVDIA现在在`Linux`上具有三个不同的驱动程序:`nouveau`开源实现的驱动程序,`nvidia`NVIDIA提供的私有驱动程序`nvidia-open`NVIDIA提供的开源驱动程序其中`nvidia-open`还在积极的开发阶段,显然是被`FUCK`太多推出的产物。
参考`Hyprland`官方文档中对于NVIDIA的支持页面需要安装`nvidia-open-dkms`包再加上几个内核参数。咔咔咔一顿操作之后,期待的等待电脑重启。`Linux`万年不变的启动信息刷过,然后电脑就黑屏了。
作为一个经验丰富的NVIDIA受害者我轻轻一搜就发现这个由于INTEL的内核驱动和NVIDIA内核驱动冲突导致的问题。然而现在不能进系统我不得不紧急学习了一波内核启动参数通过在`grub`中修改启动参数禁用`intel_i915`模块才将系统启动了起来。
装好了驱动,现在开始装桌面环境。很遗憾,默认版本的`wlroots`在NVIDIA驱动下兼容性不佳需要自行修改源代码自行编译虽然有着万能的`AUR`辅助我们,但是好巧不巧,那段时间正好遇到`Hyprland`项目调整文件结构,使用`AUR`编译失败了。于是我不得不手动`clone`下来修改之后手动编译。
历尽千难万险,终于进入了`Hyprland` 的桌面。 本来以为安装完成之后就可以愉快的享gongzuo没想到是折磨的开始。
首先是屏幕闪烁的问题,能明显感觉到屏幕会闪烁,出现的时机是完全随机的。按照`wiki`上的说明,在源代码上打上`patch`之后重新编译安装,没用。文档上还记录了一种“核武器”方式,但是需要修改内核配置和造成功耗提升,不敢试。然后是在`vscode`中遇到“屏幕冻结”的问题。这个问题在频繁上下滚动的时候特别容易出现,具体表现为滚动了但是屏幕上的一部分还是显示滚动之前的内容没有刷新。
上述问题正好发生在我发现`matlab`没法在`wayland`下正常工作的时候,一怒之下我就回到了`KDE`
已经是2023年了`Wayland`仍然是`Linux`桌面环境永远的痛。
## 双系统的奇妙bug
上文中提到我搞到了一台使用NVIDIA显卡的笔记本拥有了**强劲**图形性能怎么能不玩游戏呢。但是`Linux`对于游戏支持程序实在是一言难尽,具体可以查看这个系列前几篇我和原神的相爱相杀。于是我就选择了安装`Windows``Linux`双系统。
由于这台笔记本支持安装多个`m.2`的硬盘位,因此我就将两个系统安装到了两个不同的硬盘中,使用`grub`切换在启动时需要进入哪个系统。
### `iwlwifi`加载失败的问题
虽然安装的过程一帆风顺,但是使用的过程却是波折连连。出现的第一个问题就是`iwlwifi`这个驱动程序在从`Windows`切换到`Linux`的启动过程中会启动失败。使用`dmesg`可以观察到如下的报错:
```
iwlwifi: probe of xxxx:xx:xx:x failed with error -110
```
一通咕狗容易发现这样的一个[bug](https://bugzilla.kernel.org/show_bug.cgi?id=209641#c55),似乎是由于`Windows`使用快速启动造成的。
解决办法或者说“回避策略”有两个:
- 关闭`Windows`的快速启动
- 遇到这个问题了重启`Linux`
于是干掉快速启动。

BIN
source/posts/daily-linux-3/Screenshot_20230904_144149.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,86 @@
---
title: 日用Linux挑战 第4篇 新的开始
date: 2024-03-09T14:00:00.0000000
tags:
- Linux
- 杂谈
---
小步快跑,面向未来。
<!--more-->
在上一次更新“日用Linux挑战”之后的六个月时间中我又重新在我的两台电脑上安装了`ArchLinux`。在这一轮新的系统安装中引入了不少令人激动的新技术。
## BTRFS
在所有的新技术中最激动人心便是新的文件系统`Btrfs`作为“面向Linux的现代写时复制文件系统”`Btrfs`致力于在实现高级特性的同时保证对错误的宽容和使用与维护的便利性。
相较于古老但是稳定的`Ext4`文件系统,`Btrfs`对我来说最大的好处便是可以零成本的创建快照,便于在出现错误的时候及时回滚或者直接重装系统。因此,为了方便快照的生成和回滚,我在安装系统时使用**扁平化**的子分区划分方法:即尽力避免出现嵌套的子分区,所有需要快照的分区都处在`/`目录之下:
![Screenshot_20240309_115143](daily-linux-4/Screenshot_20240309_115143.webp)
- `@`为根分区,挂载在`/`目录之下,打开写时复制;
- `@home`为家目录分区,挂载在`/home`目录之下,打开写时复制;
- `@swap`为交换分区;
- `@var`挂载在`/var`目录之下,鉴于这下面的文件大多数为生成的数据文件,关闭写时复制;
- `@snapshots`是存储快照的子分区,挂载在`/.snapshots`目录之下,打开写时复制。
在一般情况下上每天对`@home`分区进行快照,存放在`/.snapshots`目录下,每周将当天备份的数据备份到移动硬盘中。
### 使用BTRFS的一些小提示
由于`docker`使用的默认文件系统是`overlay2`文件系统,也是一个支持写时复制的文件系统,而在写时复制文件系统`BTRFS`上再叠一层写时复制文件系统显然不好,而且`docker`官方也提供了对于`btrfs`文件系统的[支持](https://docs.docker.com/storage/storagedriver/btrfs-driver/),因此需要修改`docker`使用的文件系统,按照官方文档操作即可。
对于一些常常修改的文件夹例如`.cache`,可以关闭写时复制。
在通过`SSH`将数据备份到远程服务器时,可以使用`zstd`压缩之后再发送,可以大幅减少需要传送的数据量:
```shell
sudo btrfs send /.snapshots/home@20240225 | zstd | ssh root@remote "zstd -d | btrfs receive /path-to-backup"
```
## Wayland
算起来,我已经和`Wayland`显示协议相爱相杀了整整一年了,从`KDE plasma X``Hyprland`,再尝试小众的`labwc`,最后回到了`KDE plasma X`。而在2024年2月29日`KDE plasma`释出6.0版本,将`Wayland`作为默认的显示协议,我也在第一时间更新了版本并使用`wayland`显示协议。现在,我可以比较确定的说,`Wayland`目前已经达到可用的水平了,而且我还是使用`RTX 3060`显卡。
![image-20240309130329784](daily-linux-4/image-20240309130329784.webp)
不过相较于`AMDGPU`可以开箱即用,使用`NVIDIA`启动需要配置如下的模块参数:
```
options nvidia_drm modeset=1 fbdev=1
```
同时`NVIDIA`驱动的版本在`550`以上。如果`NVIDIA`驱动的版本`< 550`,那么就需要在内核参数中配置`nvidia_drm.modeset=1`,因为在之前的`NVIDIA`版本驱动中对于`simpledrm`的支持还不完善,需要通过内核参数禁用,~~而且这是一个`ArchLinux`提供的hack~~。
对于输入法,在使用`fcitx5`输入法并通过`Virutal Keyboard`启动输入法之后,在几乎所有的`Wayland`应用程序中都能够正常的唤起输入法,包括各种基于`chromium`的浏览器和`Electron`应用,不过需要在启动应用时传递如下的参数:
```
--enable-features=UseOzonePlatform
--ozone-platform=wayland
--enable-wayland-ime
```
不过`XWayland`应用程序在使用`NVIDIA`驱动时会存在一个神奇的**同步失败**问题,表现为在`xwayland`中部分控件闪烁,交替显示更新前和更新后的帧,而且这个问题几乎不能被截屏抓到,具体可以见`freedesktop`上的这个[issue](https://gitlab.freedesktop.org/xorg/xserver/-/issues/1317)。虽然这个议题下面有着很长的讨论,还是建议大家完整的看一遍,里面甚至还有:
![image-20240309131750535](daily-linux-4/image-20240309131750535.webp)
省流:这个议题讨论了在`xserver`中提供显式同步的协议原语,方便图形驱动程序知道什么时候渲染的帧发生了变化。因此这并不是一个`NVIDIA`驱动程序的问题,而是需要将`Linux`显示协议栈从隐式同步迁移到显式同步。但是相关的工作还在开发过程中,因此解决方法有两个:
- 避免使用`xwayland`应用;
- 自行编译该合并请求[!967](https://gitlab.freedesktop.org/xorg/xserver/-/merge_requests/967),或者使用这个[xorg-xwayland-explicit-sync-git](https://aur.archlinux.org/packages/xorg-xwayland-explicit-sync-git)。
## 安装双系统获得的新知识
在这轮安装过程中,为了保证在极端情况下的可用性,我都是选择了双系统`Windows``ArchLinux`进行安装。但是这也带来了一个问题:`Windows`安装的过程中创建的`EFI`分区只有100M的空间而现在`Linux`内核的大小一般是`14M`左右,而`initramfs`的大小来到了`24M`上下,再加上一个更大的备用`initramfs`~~装不下,怎么想也装不下~~,分分钟撑爆`/boot`分区。
于是,我就在`Arch Wiki`上学到一条新知识:
![image-20240309134847166](daily-linux-4/image-20240309134847166.webp)
原来`efi`分区其实只用放`grub`
![img](daily-linux-4/cfd17cff0701a8e8c69fecf247f17fc1-1709963611271-2.webp)

BIN
source/posts/daily-linux-4/Screenshot_20240309_115143.webp (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
source/posts/daily-linux-4/image-20240309130329784.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/daily-linux-4/image-20240309131750535.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/daily-linux-4/image-20240309134847166.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,626 @@
---
title: 从.NET 6到.NET 8中的性能提升综述
date: 2024-08-31T18:51:06.9233598+08:00
tags:
- dotnet
- 技术笔记
- 编译原理
---
JIT编译就一定比AOT编译慢吗
<!--more-->
长期以来我们已经习惯了调侃Java较慢的运行速度并将其原因归咎于Java使用了字节码加虚拟机的JIT编译方式。但是对于同样采用了这样方式的.NET微软的开发人员却认为——"虽然这样说很难让人信服但是许多人都认为托管应用程序的性能实际上超过了非托管应用程序。有许多原因使我们对此深信不疑。例如当JIT编译器在运行时将IL代码编译成本地代码时编译器对于执行环境的认识比非托管编译器更深刻。"摘自Jeffrey Richter的《CLR via C#》)。
既然微软的开发人员对此深信不疑,同时在.NET Core之后.NET的内部开发流程逐渐公开化在[Github](https://github.com/dotnet/runtime)和[.NET 官方博客](https://devblogs.microsoft.com/dotnet/)上都能看到。那么我们就在本篇文章中梳理一下.NET平台从.NET 6到.NET 8三个版本中所有主要的性能提升主要聚焦于JIT编译器、内存管理等少数几个部分。
## JIT
JITJust In Time即时编译编译器是运行时中的基础负责将前端编译器生成的ILIntermediate Language就是一套微软规定的中间表示形式转换为汇编语言在AOTAhead Of Time提前编译编译时也是调用的该编译器。这里可以解释一下.NET代码执行的三种模型
- JIT编译执行最为“传统“的执行模型所有的IL代码都需要在执行前通过JIT编译为本机代码再执行。
- 即时运行ReadyToRun R2R)在程序编译阶段先调用JIT编译器将IL代码编译为本机代码在程序运行时首先运行编译好的本机代码以提高应用启动的速度在运行过程中再次调用JIT编译对热点代码进行优化编译。为了提高启动速度.NET中的所有核心库都以R2R的形式提供程序员可以自行决定编写的代码是否使用R2R的方式运行。
- 提前编译在程序编译阶段直接调用JIT编译器将IL代码编译为本机代码程序运行时就执行这一套代码。
### 分层编译、栈上替换和PGO
在.NET从6到8的版本演进过程中最为重磅的性能更新莫过于在.NET 6便引入的动态PGOdynamic Profile-Guided Optimization在.NET 8中终于默认启用了。为了介绍动态PGO我们必须首先理解JIT对于IL代码的分层编译机制。
#### 分层编译
在JIT编译器最初的设计模型中每个方法只会被编译一次每个方法只会在调用被编译为汇编代码该代码被缓存起来以备下次调用。但是这种设计却导致许多矛盾一个根本性的矛盾就是JIT编译花费在编译优化上的时间同从优化中能得到的效果之间的矛盾。在编译过程中对代码进行优化几乎是编译器工作过程中最耗时的部分尤其是对于一个JIT编译器来说编译的时间几乎直接决定了应用启动的时间如果对一个方法进行优化需要耗费一秒钟的时间但是仅能使该方法的运行时间从10毫秒下降到1毫秒在该方法在运行过程只会调用一次的情况下编译器引入该优化只会让程序的运行时间增加。因此编译器必须要在程序运行时的效率和启动时间之间做取舍。尤其是考虑到程序的**空间局部性**原理:程序中的大多数函数只会在运行时被调用少数几次,对于这些函数在启动时耗费大量的优化时间是纯纯的浪费。
**分层编译**的引入从根本上解决了这个问题:该编译策略允许一个方法在运行时被编译多次。
在第一次调用时方法会被编译到第0层Tier 0。在这个编译层级上只会应用少数的编译优化策略这些编译优化策略被称为最小优化策略Minimal OptimizationMin Opts。需要指出的这些策略实际上也不少包含了那些可以使JIT编译器更快运行的优化策略例如可以生成更少量的本机代码。在优化的同时JIT编译器还会注入一些短短的代码片段stub这些代码片段使得运行时可以统计每个方法的调用次数。
运行时可以监控这些方法的调用次数当某个方法的调用次数超过某个预先设定的阈值时这个方法将被加入重新编译的队列。这次编译将会把方法编译到第1层Tier 1JIT编译器将会在编译的过程中应用所有可能的优化策略。在整个程序的运行过程中只有少数被多次调用的方法会编译到第1层。同时编译器也可以通过收集方法在第0层的运行过程中的信息来进行第1层编译过程中的优化。例如对于`static readonly`类型的变量当方法在第0层执行之后这些类型的变量已经完成初始化且无法再发生更改此时编译器就可以将这些变量当作是`const`类型的常量,将所有应用于常量的优化策略扩展到该类型的变量上进行应用。
在大多数情况下使用分层编译可以使用程序同时获得良好的启动速度和运行效率除了某些特定的情况。这些特定情况的一个典型例子就是运行时间非常长的方法在上述的优化策略中只重视了调用次数非常多的方法但是运行时间非常长的方法也对于效率有着非常明显的影响。而在分层编译的情况下这些长运行时间但是少调用次数的函数将会只被编译到第0层这会造成明显的性能下降。因此在.NET 7之前所有含有回溯分支的方法都会直接编译到第1层。
.NET 7引入的栈上替换改进了这一点。
>这里可能有人会争论:对于少数运行时间长的方法在启动时多施加一些优化策略真的会导致明显的启动时间增加吗,有必要引入更复杂的策略针对这点蚊子腿进行优化吗?
>
>的确对这点启动时间进行优化很可能是不明显的但是别忘了编译器可以在第0层的运行过程中收集信息进行第1层的优化这实际上也是动态PGO机制引入的基础之一。
#### 栈上替换
分层编译很好,除了在面对运行时间长的方法时。例如对于下面这个包含一万次循环的方法:
```csharp
class Program
{
static void Main()
{
var sw = new System.Diagnostics.Stopwatch();
while (true)
{
sw.Restart();
for (int trial = 0; trial < 10_000; trial++)
{
int count = 0;
for (int i = 0; i < char.MaxValue; i++)
if (IsAsciiDigit((char)i))
count++;
}
sw.Stop();
Console.WriteLine(sw.Elapsed);
}
static bool IsAsciiDigit(char c) => (uint)(c - '0') <= 9;
}
}
```
当在.NET 6平台运行时我们可以比较在启用对方法的分层编译和不启用的情况下的性能对比。
| 编号 | 启用分层编译 | 不启用 |
| ---- | ---------------- | ---------------- |
| 1 | 00:00:01.2841397 | 00:00:00.5734352 |
| 2 | 00:00:01.2693485 | 00:00:00.5526667 |
| 3 | 00:00:01.2755646 | 00:00:00.5675267 |
| 4 | 00:00:01.2656678 | 00:00:00.5588724 |
| 5 | 00:00:01.2679925 | 00:00:00.5616028 |
栈上替换On Stack Replacement就是为了解决这个问题而引入的没人规定说一个方法执行的本机代码只能在执行的间隙被替换在执行的过程中也可以替换掉方法执行的本机代码也就是当方法还在运行栈上时执行替换。在第0层编译时编译器不仅可以为函数的调用生成统计调用次数的片段代码也可以为循环的执行生成运行次数的片段代码。当运行时监控到某一个循环的执行次数超过设定的阈值时编译器就可以将该方法编译到第1层运行时将会把方法此时调用的所有寄存器和本地变量复制到一个新的方法调用中而新的调用使用的本机代码已经是优化之后的本机代码了。
在分层编译和栈上替换的协作下程序的启动实现和运行性能之前就可以达到一个较好的平衡了。当然分层编译和栈上替换的能力并不仅限于优化应用的启动时间在动态PGO中这两者将会发挥更大的作用。
#### 动态PGO
采样制导的优化Profile-Guided Optimization并不是一个新鲜的概念在数十年前就出现并在多种编程语言和运行时中得到的应用。PGO的一个典型工作流程一般如下
1. 在插入一些特定指令的情况下构建应用程序;
2. 将应用程序放在典型的应用场景下进行运行,并通过这些特定指令收集运行的信息;
3. 在这些信息的指导下重新构建应用程序,得到针对运行场景的特定优化。
这种工作流程被称作是静态的PGO这些工作流往往额外的应用知识、特定的工具和构建-上线流程的反复执行。
回到.NET的执行过程中既然分层编译已经可以将程序生成为第0层和第1层两个版本为什么不在第0层程序的运行过程中收集一些有用的信息输入到第1层的编译过程中呢这样编译器还可以生成更加优化的第1层本机代码。这个过程中传统静态PGO流程中的构建-运行-再构建流程完全一致不过现在优化的层级可以聚焦在方法上而不是针对整个程序进行优化以及最为重要的是这一切都在程序运行的过程中由JIT编译器自动的进行不需要任何额外的开发工作或者是针对性的构建流程。
在.NET 6到.NET 8整整三个大版本对于动态PGO的迭代过程中引入了大量的优化这里仅能介绍一小部分。
首先是为了更好发挥动态PGO的性能JIT编译器中为分层编译引入了更多的编译层数。需要引入更多编译层数的原因主要有两点。第一插入各种采样的指令和代码是需要代价的考虑到第0层编译的主要目标是为了降低编译的时间提高应用的启动速度在第0层编译过程中就不能插入太多的采样指令。因此编译器首先增加了一个新的编译层——采样第0层来解决这个问题。大部分的方法将在第一次运行时编译到缺少优化、缺少采样指令的第0层在运行时发现该方法被调用了多次之后JIT编译器将这个方法重新编译到采样第0层再经过一系列的调用之后JIT编译器将利用采样得到的信息对该方法重新进行编译并优化。第二在原始编译器模型中使用即时运行R2R方法编译的代码不能参加到动态PGO中尤其是考虑到几乎所有应用程序都会调用的核心库代码是采用R2R的方式进行运行的如果这部分的代码不能参加动态PGO将不能够完全发挥动态PGO的效果虽然核心库在提前编译的过程中会使用静态PGO进行一部分的优化。因此JIT编译器为R2R编译好的代码增加了一个新的编译器在运行时发现这部分代码被调用多次之后将会被JIT编译器编译到含有优化和采样代码的采样第1层随着调用次数的增加这部分的代码将可以利用采样得到的信息进行优化。下面这张图展现了不同编译方法在运行过程中可能达到的编译层级。
![image-20240828135354598](./dotnet-performance-8/image-20240828135354598.webp)
JIT编译器也在第0层编译的过程中引入了更多的优化。虽然第0层编译的目的是缩短编译的时间但是许多的优化可以通过减少需要生成的代码数量来达到这个目的。常量折叠Constant Folding就是一个很好的例子。虽然这会让JIT编译器在第0层编译时花费更多的时间同运行时中的虚拟机交互来解析各种变量的类型但是这可以大量的减少JIT编译器需要生成的代码量尤其是对于下面这种涉及到类型判断的例子。
```csharp
MaybePrint(42.0);
static void MaybePrint<T>(T value)
{
if (value is int)
{
Console.WriteLine(value);
}
}
```
现在在第0层编译的过程中JIT编译器可以发现`MaybePrint`方法在运行过程中不会运行任何实际的代码路径,因此可以直接优化掉这段代码。
```assembly
; Assembly listing for method Program:<<Main>$>g__MaybePrint|0_0[double](double) (Tier0)
; Emitting BLENDED_CODE for X64 with AVX - Windows
; Tier0 code
; rbp based frame
; partially interruptible
G_M000_IG01: ;; offset=0x0000
push rbp
mov rbp, rsp
vmovsd qword ptr [rbp+0x10], xmm0
G_M000_IG02: ;; offset=0x0009
G_M000_IG03: ;; offset=0x0009
pop rbp
ret
; Total bytes of code 11
```
插入的采样代码片段也会造成一些性能上的问题。为了优化JIT编译器往往需要统计各种方法和分支的调用和运行次数但是问题是这些统计调用次数的代码应该如何编写尤其是考虑到代码片段是一个静态的“数据”会在各种不同的运行线程之间共享如何设计一个线程安全同时高效的统计方法
最初的统计方式是设计一个朴素、没有线程同步的方法,例如`_branches[branchId]++`。虽然这种方法没有在运行时引入大量的同步开销但是这也意味着在某个方法被多个线程同时调用时会损失掉大量的统计数据这会造成一个本应该提前进入动态PGO的方法得到优化的时间严重滞后。这方面一个容易想到的方式是使用同步的方法进行统计例如给数据加锁或者是使用原子指令`Interlocked.Add`。但是这种方式会严重的导致性能下降。为了解决这个问题开发者们设计了一种非常巧妙的解决方法这种方法的C#实现如下所示
```csharp
static void Count(ref uint sharedCounter)
{
uint currentCount = sharedCounter, delta = 1;
if (currentCount > 0)
{
int logCount = 31 - (int)uint.LeadingZeroCount(currentCount);
if (logCount >= 13)
{
delta = 1u << (logCount - 12);
uint random = (uint)Random.Shared.NextInt64(0, uint.MaxValue + 1L);
if ((random & (delta - 1)) != 0)
{
return;
}
}
}
Interlocked.Add(ref sharedCounter, delta);
}
```
在计数器的值没有超过8192时计数逻辑直接使用原子指令进行统计。当计数器的数值超过8192之后计数逻辑将采用一个随机的增加策略。首先按照50%的概率给计数器增加2然后按照25%的概率增加4然后按照12.5%的概率增加8依次类推。随着计数器值的增加但是需要调用原子指令的频率也就越低。
为了验证该计数逻辑的有效性,可以使用下面的代码进行验证。
```csharp
using System.Diagnostics;
uint counter = 0;
const int ItersPerThread = 1_000_000_00;
while (true)
{
Run("Interlock", _ =>
{
for (int i = 0; i < ItersPerThread; i++) Interlocked.Increment(ref counter);
});
Run("Racy ", _ =>
{
for (int i = 0; i < ItersPerThread; i++) counter++;
});
Run("Scalable ", _ =>
{
for (int i = 0; i < ItersPerThread; i++) Count(ref counter);
});
Console.WriteLine();
}
void Run(string name, Action<int> body)
{
counter = 0;
long start = Stopwatch.GetTimestamp();
Parallel.For(0, Environment.ProcessorCount, body);
long end = Stopwatch.GetTimestamp();
Console.WriteLine(
$"{name} => Expected: {Environment.ProcessorCount * ItersPerThread:N0}, Actual: {counter,13:N0}, Elapsed: {Stopwatch.GetElapsedTime(start, end).TotalMilliseconds}ms");
}
```
运行得到的数据如下所示:
| 类型 | 期望数值 | 实际数值 | 运行时间 |
| -------- | ------------- | ------------- | ------------ |
| 原子指令 | 2,000,000,000 | 2,000,000,000 | 22241.9848ms |
| 朴素 | 2,000,000,000 | 220,525,235 | 277.3435ms |
| 随机 | 2,000,000,000 | 2,024,587,268 | 527.5323ms |
从数据上就可以发现,新方法可以在和朴素方法接近的运行时间下获得和使用原子指令接近的实际数值,而且运行时间会随着数值的增加进一步的减少,逐渐逼近朴素方法的运行时间。
如何准确而低成本的技术并不是采样过程中唯一的问题。另一个问题是如何统计在接口或者是虚拟方法调用时哪个类型是最可能被调用到的类型如果JIT能够得到这种信息就可以为该类型生成一条更加快速的调用路径。正如上一个算法所揭示的准确统计每一个类型被调用的次数是非常昂贵的因此在这里开发者引入了一种被称作蓄水池采样Reservoir Sampling的方法进行统计。例如对于一个含有60%的`'a'`、30%的`'b'`和10%的`c'`的字符序列,如何快速而准确的统计其中哪个字符出现的频率最高?利用蓄水池采样算法,可以写出如下的统计代码:
> 蓄水池采样算法设计的目的是为了解决这样一个问题:给出一个数据流,这个数据流的长度很大或者是未知,并且对于该数据流中的数据只能访问一次。请设计一个随机选择算法,使得数据里中所有数据被选中的概率相等。
```csharp
// Create random input for testing, with 60% a, 30% b, 10% c
char[] chars = new char[1_000_000];
Array.Fill(chars, 'a', 0, 600_000);
Array.Fill(chars, 'b', 600_000, 300_000);
Array.Fill(chars, 'c', 900_000, 100_000);
Random.Shared.Shuffle(chars);
for (int trial = 0; trial < 5; trial++)
{
// Reservoir sampling
char[] reservoir = new char[32]; // same reservoir size as the JIT
int next = 0;
for (int i = 0; i < reservoir.Length && next < chars.Length; i++, next++)
{
reservoir[i] = chars[i];
}
for (; next < chars.Length; next++)
{
int r = Random.Shared.Next(next + 1);
if (r < reservoir.Length)
{
reservoir[r] = chars[next];
}
}
// Print resulting percentages
Console.WriteLine($"a: {reservoir.Count(c => c == 'a') * 100.0 / reservoir.Length}");
Console.WriteLine($"b: {reservoir.Count(c => c == 'b') * 100.0 / reservoir.Length}");
Console.WriteLine($"c: {reservoir.Count(c => c == 'c') * 100.0 / reservoir.Length}");
Console.WriteLine();
}
```
程序的输出是5次次采样统计的结果
![image-20240828155556375](./dotnet-performance-8/image-20240828155556375.webp)
需要指出的是,虽然在上面的代码中使用和运行时代码中一样的“蓄水池”大小,但是在运行时并没有提前获得所有需要统计的数据,调用的统计数据是由多个不同的运行线程同时写入蓄水池中的。从结果中可以看出,虽然数值上并不准确,但是该算法准确的统计出了各个字符的出现趋势。
在上述两个例子中算法中都引入了随机数的概念进行统计这就导致每次运行的结果都在一定程度上有着不同同时这也会导致在每次程序运行的过程中动态PGO所做的优化都会有轻微的不同。有的开发者可能会担心这些随机的引入是否会造成程序运行行为的不可确定性从而导致程序的调试变得困难但是实际上在引入这些随机数之后这些代码路径已经就有一定的不确定性例如那个朴素的调用次数统计算法同时开发过程中已经有大量的数据证实这些代码的行为是总体上稳定且可重现的。
本篇文章中介绍动态PGO的部分就大致到这里但是文章后续的部分中仍然可以在各个地方中看到动态PGO的身影这也可以侧面看出动态PGO对于整个优化的巨大作用。
### 函数内联
函数内联是JIT编译器能完成的重要优化之一其的运行逻辑是取消对于某个方法的直接调用而是将该方法的执行代码直接插入到当前的控制流中。函数内联最显而易见的优化是减小了调用函数过程中压栈和弹栈带来的开销但除了对于某些在热点路径上的小型方法这点减少的开销实际上并不是函数内联实际上带来的主要优化。
函数内联带来的主要优化是其将被调用者的逻辑暴露给了调用者,或者反过来。例如,当调用者将一个常数作为参数传递给被调用的方法时,如果被调用的方法没有进行内联,对该方法进行编译时编译器就无从得知一个常数被传递了过来,但是如果该方法被内联了,进行编译的编译器就可以应用一切对于常数可以应用的优化,包括删除死代码、分支预测、常量折叠等等。
按照这个逻辑分析那么在编译的时候应该应内联尽内联但是内联有可能会增加编译之后的指令条数。而指令条数的增加可能会造成指令缓存效率的下降——当需要读取内存的次数越多时缓存的效率就会越低。例如考虑一个方法这个方法在整个程序中被内联了100次而这一百次都内联编译为一份不同的本机代码序列这一百次调用就完全不能高效的利用指令缓存而如果对于这个方法没有进行内联这一百次调用都可以指向同一个内存地址这就让指令缓存感到非常舒适。因此在JIT编译器编译一个方法时如果编译器聪明到可以判断出内联之后编译得到的指令序列将少于直接调用得到的指令序列那么编译器就可以执行内联操作反之编译器就需要衡量内联方法得到的吞吐量提高和增长的指令序列造成的运行效率了。
因此就需要JIT编译器合理的判断哪些方法在编译过程中需要进行内联哪些方法在编译过程中进行内联。这方面编译器做出的主要更新是让内联更好的能够判断需要被内联方法的内容尤其是在方法没有被分层编译或者是方法直接跳过了第0层编译的情况下。再考虑到在运行时库中引入的大量可以低成本调用的硬件加速指令方法这些方法也可以有效的进行内联。
### 去虚拟化
在调用一个接口类型的变量上的方法时,运行时需要做的一个重要工作就是判断实际上应该调用哪个类型的对象上的方法,这在对于接口、虚拟成员方法、泛型方法和委托类型的调用上都是适用的。
因此JIT编译器引入一种被称为保险去虚拟化Guarded DevirtualizationGDV的机制进行优化这种机制也是在动态PGO的帮助下引入的。具体地说在运行时将会统计具体被调用的类型或者方法的频率然后在进行优化编译时为最常出现的类型提供一条快速调用的路径。对于下面这种例子来说
```csharp
public class Tests
{
internal interface IValueProducer
{
int GetValue();
}
class Producer : IValueProducer
{
public int GetValue() => 42;
}
private IValueProducer _valueProducer;
private int _factor = 2;
public void Setup() => _valueProducer = new Producer42();
public int GetValue() => _valueProducer.GetValue() * _factor;
}
```
对于其中的`GetValue`方法在没有动态PGO和GDV的参与下这个方法中将会被编译为一种普通的接口方法调用。但是在启用了动态PGO的环境下编译器将会注意到对于`IValueProducer`最常见的实现是`Producer`这样JIT编译器就可以为`Producer`生成一条快速路径对应与下面的C#实现
```csharp
int result = _valueProducer.GetType() == typeof(Producer) ?
Unsafe.As<Producer>(_valueProducer).GetValue() :
_valueProducer.GetValue();
return result * _factor;
```
.NET中实现的GDV优化可以支持生成多个GDV也就是在进行接口调用同时为多个类型生成快速路径。但是这个默认的运行条件下是关闭需要用户通过一个特定的环境变量进行设置`DOTNET_JitGuardedDevirutalizationMaxTypeChecks`。这一优化在使用AOT编译器直接编译到本机代码时还有一个非常有趣的效果考虑到在进行AOT编译时会对程序集进行裁剪也就是删除掉最终的应用程序中没有用到的类型这就让编译器可以在编译时知道实现了某一特定接口的类型总共有哪些并且在这些类型的数量较少时直接为这些类型都生成调用时的快速路径而完全避免在运行时进行判断。
在上文中已经提到GDV不仅可以在调用接口上定义方法时使用也可以在调用委托的时候使用。这使用GDV在和循环克隆Loop Cloning等优化技术配合时能够发挥出更大的功能例如对于下面这个例子
```csharp
public class Tests
{
private readonly Func<int, int> _func = i => i + 1;
public int Sum() => Sum(_func);
private static int Sum(Func<int, int> func)
{
int sum = 0;
for (int i = 0; i < 10_000; i++)
{
sum += func(i);
}
return sum;
}
}
```
在上面的示例代码的循环中调用了一个委托`func`在动态PGO和GDV的参与下编译器可以知道这个委托最常见的实现其实是唯一的是一个固定的Lambda函数暂且称之为Known Lambda因此编译器可以将`Sum`函数的编译器为如下的等价C#代码
```csharp
private static int Sum(Func<int, int> func)
{
int sum = 0;
for (int i = 0; i < 10_000; i++)
{
sum += func.Method == KnownLambda ? i + 1 : func(i);
}
return sum;
}
```
> 这里需要注意的是,这些代码都是**等价**C#代码实际上编译器并不是先编译为一种C#形式的代码,而是直接生成为汇编代码。
显然在循环内部反复的进行一个相同的判断并不是一个理想的状态。因此在变量提升hoisting优化技术的帮助下编译器可以将循环内部一个相同的判断提升到循环外部执行这将产生如下的等价代码。
```csharp
private static int Sum(Func<int, int> func)
{
int sum = 0;
bool isAdd = func.Method == KnownLambda;
for (int i = 0; i < 10_000; i++)
{
sum += isAdd ? i + 1 : func(i);
}
return sum;
}
```
这还不是优化的极限,注意到在每个循环中还有个重复的三元表达式,这个的结果在各次循环之前也应该是稳定的,因此在循环克隆优化的指导下,编译器将生成如下的等价代码。
```csharp
private static int Sum(Func<int, int> func)
{
int sum = 0;
if (func.Method == KnownLambda)
{
for (int i = 0; i < 10_000; i++)
{
sum += i + 1;
}
}
else
{
for (int i = 0; i < 10_000; i++)
{
sum += func(i);
}
}
return sum;
}
```
这可以说在动态PGO和GDV优化策略的加持下一些“传统的”优化策略又被编译器榨出了新的潜能从实际的跑分上也可以验证这惊人的优化。
| 方法 | 条件 | 平均运行时间 |
| ---- | ---------------- | ------------ |
| Sum | 开启动态PGO和GDV | 2.320us |
| Sum | 关闭动态PGO和GDV | 16.546us |
### 分支
分支代码几乎是所有的代码片段中都会涉及到的模式,包括各种循环、判断和三元表达式种种。但是考虑到现代处理器都是多发射的超标量流水线处理器,而各种分支代码往往会打断这些高速运行的流水线,尽管处理器的设计者会通过分支预测器等技术进行猜测,而且往往还猜得很准,但是如果预测出错就需要清空流水线重新运行。因此如何减少代码中的分支是编译器优化的重要课题。
删除重复的分支判断是一个常见的分支优化,尤其常见与用户代码和库代码进行交互的过程中。例如对于下面这个例子:
```csharp
public ReadOnlySpan<char> SliceOrDefault(ReadOnlySpan<char> span, int i)
{
if ((uint)i < (uint)span.Length)
{
return span.Slice(i);
}
return default;
}
```
这段代码中首先判断索引起始的位置是否小于切片的长度再调用对应的切片方法,但是在`ReadOnlySpan<char>.Slice`的源代码中还有一个几乎一致的判断:
```csharp
public ReadOnlySpan<T> Slice(int start)
{
if ((uint)start > (uint)_length)
ThrowHelper.ThrowArgumentOutOfRangeException();
return new ReadOnlySpan<T>(ref Unsafe.Add(ref _reference, (nint)(uint)start /* force zero-extension */), _length - start);
}
```
这就让生成的本机代码中出现两个冗余的判断。编译器可以针对这种冗余的判断进行检查并删除这些重复判断。这种类似的分支删除后面还会在“消除边界检查”章节中提到。
灵活的应用各种位运算也是一种常见的分支优化策略。例如对于下面这种对于一个有符号整数的判断`i >= o && j >= 0`可以直接被优化为`(i | j) >= 0`,通过引入一个位运算就减少了一个分支判断。除了灵活的应用位运算之外,使用指令集提供的各种条件移动指令也是一种有效的分支优化策略,比如`x86/64`指令集中提供的`cmov`指令和`arm`指令集中提供的`csel`指令,这些指令都将一个条件判断封装到一条指令中。
C#编译器也可以在分支消除中贡献一份属于自己的力量。考虑.NET中非常常见的一个类型`System.Boolean`,在使用中这个类型是一个两值类型,有且仅有两个取值`true``false`。但是实际上在运行时中会使用一个字节大小的空间来存储一个类型这意味实际上该类型有着256个取值并且将0视为`false`,将`[1,255]`视为`true`。当然开发者可以使用`unsafe`代码绕过一些编译器的限制但是“普通的”的开发者和核心库都只会给这个类型的赋予0或者1两个值。因此在设计一类特殊的算法——无分支判断算法时开发者可能会写出如下的代码
```csharp
static int ConditionalSelect(bool condition, int whenTrue, int whenFalse) =>
(whenTrue * condition) +
(whenFalse * !condition);
```
但是上述的代码并不能被C#接受因为C#编译器限制不能让布尔类型参加运算,因此这类算法的开发者不得不因此引入两个多余的分支判断:
```csharp
static int ConditionalSelect(bool condition, int whenTrue, int whenFalse) =>
(whenTrue * (condition ? 1 : 0)) +
(whenFalse * (condition ? 0 : 1));
```
但是现在C#编译器可以消除掉这两个多余的分支判断,因为在.NET世界中编译器可以确保布尔变量的取值只能有1或者0两种情况。
#### 消除边界检查
.NET提供的一种特性就是运行时安全这其中重要的一点就是对于数组、字符串和切片在运行时进行边界检查。但是这些边界检查就会在实际生成的代码中生成大量的分支判断这会导致程序运行的效率严重下降。因此如何让编译器在能够保证访问安全的情况下消除掉部分不必要的边界检查是编译器优化中的一个重要课题。
例如在一个常用数据结构——哈希表中,通常的实现是计算键的哈希值,并利用该哈希值作为下标在数组中获得存储的对象。考虑到哈希值是一个`int`类型的变量但是哈希表中很少需要存储高达21亿对象因此往往需要对哈希值取模之后再作为数组的下标此时取模的值常常就是数组的长度。也就是说在这种情况下对于数组的访问是不可能出现越界的情况下。因此编译器可以为类似与如下的代码取消访问数组时的边界检查
```csharp
public class Tests
{
private readonly int[] _array = new int[7];
public int GetBucket() => GetBucket(_array, 42);
private static int GetBucket(int[] buckets, int hashcode) =>
buckets[(uint)hashcode % buckets.Length];
}
```
同样的,对于下面这些代码,编译器也可以取消访问数组时的边界检查:
```csharp
public class Tests
{
private readonly string _s = "\"Hello, World!\"";
public bool IsQuoted() => IsQuoted(_s);
private static bool IsQuoted(string s) =>
s.Length >= 2 && s[0] == '"' && s[^1] == '"';
}
```
### 常量折叠
常量折叠Constant Folding同样是一个编译器在生成代码时可以进行的重要优化这让编译器在计算在编译器时就可以确定的值而不是让他们留到运行时进行。最朴素的常量折叠——例如计算一个数学表达式的值——在这里不在赘述。在上面介绍函数内联时也涉及到了常量折叠的内容分层编译的引入也会使得常量折叠的应用范围变广这些都不在这里重复。
进行常量折叠优化时一个重要的问题是“教会”编译器哪些变量是常量。这方面编译器得到的提升有:
- 可以将一个字面值字符串的长度视为一个常数;
- 在进行空安全的检查时字面值字符串是必定不为空的;
- 编译器在编译时除了可以进行一些简单的数学运算,现在整个`System.Math`命名空间中提供的算法都可以在编译时进行运算;
- `static readonly`类型的字符串和数组长度被视为一个常数;
- `obj.GetType()`现在在JIT编译器明确了解类型的情况下可以被替换为一个常量
- `DateTime`等时间类型初始化时可以在编译期计算内存存储的时间。例如对于`new DateTime(2023, 9, 1)`将会直接被编译到`new DateTime(0x8DBAA7E629B4000)`。
上述这些并不能完全覆盖在.NET 6到.NET 8三个大版本之中引入的所有JIT编译器优化但是从中也可以一窥编译器优化的精巧之处。首先编译器的优化并不是一个个独立优化策略的组合而且各种优化策略的有机组合。方法的内联就是一个典型例子通过将被调用方法的内容暴露给调用者或者反过来让其他的各种优化策略发挥更大的作用。其次JIT编译器在编译优化方面可以发挥更伟大的作用。通过在程序运行时对于运行环境和程序本身有着更加深刻的理解JIT编译器可以在运行时发挥出更高的性能。
## 内存管理
.NET中的垃圾回收器GC负责管理应用程序的内存分配和释放。每当有对象新建时运行时都会将从托管堆为对象分配内存主要托管堆中还有地址空间运行时就会从托管堆为对象分配内存。不过内存并不是无限的垃圾回收器就负责执行垃圾回收来释放一些内存。垃圾回收器的优化引擎会根据所执行的分配来确定执行收回的最佳时机。
.NET中内存管理中的一个显著变更为将内存的抽象从段Segment修改为区域Region。段和区域之前最明显的区别是大小段是较大的内存——在64位的机器上一个段的大小万网是1GB、2GB或者是4GB而区域是非常小的单元在默认情况下只有4MB的大小。从宏观上来说之前的GC是为每个代的堆维持一个GB级别的内存范围而现在GC则是维持了许多个较小的内存区域这些内存区域可以被分配给各个代的堆或者其他可能涉及的堆使用。
垃圾回收器中还有两个引人注意的特性增加。第一个是动态的代提升和下降Dynamic Promotion and Demotion`DPAD`第二个是动态适应应用程序大小Dynamic Adaptive To Application Size`DATAS`)。`DPAD`特性允许GC在工作的过程中动态的设置一个区域的代数例如直接将一个可能存活时间非常长的对象配置为第2代而这在之前的GC模型中需要通过两次垃圾回收才能实现。而第二个特性`DATAS`旨在适应应用程序的内存要求即应用程序堆的大小和长期数据大小大致成正比即使在不同规格的计算机上执行相同的工作时运行时中堆的大小也是类似的。相比如下传统的服务器模式下的GC旨在提高程序的吞吐量允许内存的分配量基于吞吐量而不是应用程序的大小。`DATAS`对于各种突发类型的工作负载是非常有利的,同时通过允许堆大小按照工作负载的要求进行调整,这将让一些内存首先的环境直接受益。
### 无垃圾回收的堆
在程序中大量会涉及到使用常量字符串的情形,例如下面这个例子:
```csharp
public class Tests
{
public string GetPrefix() => "https://";
}
```
在.NET 7平台上这个方法会被JIT编译器编译之后得到下面这段本机代码
```assembly
; Tests.GetPrefix()
mov rax,126A7C01498
mov rax,[rax]
ret
; Total bytes of code 14
```
在这段代码中使用了两个`mov`指令,其中第一个指令加载存储这个字符串对象地址的地址,第二个读取该地址。从这段本机代码可以看见,尽管已经是在处理一个常量的字符串,但是编译器和运行时仍然需要为这个字符串在堆上分配一个`string`对象因为一个在堆上分配的对象在GC的控制下会在内存中发生移动编译器就不能为这个对象使用一个固定的内存地址需用从一个指定的地址读取该对象所在的地址。如果能让这个常量字符串分配在不会移动的内存区域中就能从编译器和GC两个方面上提高程序运行的效率。
为了优化这种生成周期和程序一致对象的内存管理,.NET 8中引入了一个新的堆——没有内存管理的堆。JIT编译器将会保证这些常量类型的对象将会被分配在这个堆中这种没有GC管理的堆也意味着JIT编译器可以为这些对象使用一个固定的内存地址在使用时避免掉了一次内存读取。
![Heaps where .NET Objects Live](./dotnet-performance-8/HeapsWhereNetObjectsLive.webp)
将上述提高的示例代码使用.NET 8版本进行编译得到的代码如下从中也可以看出JIT编译器生成的代码只有一条`mov`指令,避免了一次内存访问。
```assembly
; Tests.GetPrefix()
mov rax,227814EAEA8
ret
; Total bytes of code 11
```
这个没有内存管理的堆引入还可以让其他的类型受益。例如对于`typeof(T)`返回的类型对象,容易想到一个程序集中所有类型对象的生命周期应该是和程序一致的,因此也可以在这个堆上分配所有这些类型对象。`Array.Empty<T>`也可以利用类似的思路分配在这个堆上。
### 值类型
因为可以避免在堆上分配内存,值类型已经在.NET的高性能代码中得到了广泛的应用虽然频繁的内存拷贝可能带来额外的性能开销。因此编译器对于值类型的各种优化就显得至关重要。
这部分优化中一个引人注目的点是值类型的“推广”promotion这里的推广意味着将一个结果划分为组成它的各种字段来区别对待。可以利用下面这个示例代码进行理解
```csharp
public class Tests
{
private ParsedStat _stat;
[Benchmark]
public ulong GetTime()
{
ParsedStat stat = _stat;
return stat.utime + stat.stime;
}
internal struct ParsedStat
{
internal int pid;
internal string comm;
internal char state;
internal int ppid;
internal int session;
internal ulong utime;
internal ulong stime;
internal long nice;
internal ulong starttime;
internal ulong vsize;
internal long rss;
internal ulong rsslim;
}
}
```
在这段代码中有一个较大的结构类型其的大小是80个字节。在没有启用推广的条件下进行运行`GetTime`方法编译得到的本机代码如下所示。在汇编代码中将下载栈上分配一片88字节的空间再将整个结构体直接复制到当前方法的栈上在复制完成之后计算两个字段的和并返回。
```assembly
; Tests.GetTime()
push rdi
push rsi
sub rsp,58
lea rsi,[rcx+8]
lea rdi,[rsp+8]
mov ecx,0A
rep movsq
mov rax,[rsp+10]
add rax,[rsp+18]
add rsp,58
pop rsi
pop rdi
ret
; Total bytes of code 40
```
而在打开推广的情况下运行得到的本机代码如下所示:
```assembly
; Tests.GetTime()
add rcx,8
mov rax,[rcx+8]
mov rcx,[rcx+10]
add rax,rcx
ret
; Total bytes of code 16
```
在这段汇编代码中JIT编译器只复制了两个需要使用的字段到当前方法的栈上这就大幅减少了值类型在方法调用之前产生内存复制开销。
## 还有更多……
行文至此,本篇已经字数超过一万字了,毫无疑问这将成为博客历史上最长的一篇文章。在这点字数中我们还只是**简略**的介绍了一下.NET平台过去的几个版本中涉及到的优化还主要聚焦于JIT编译器和内存管理的部分在这两个部分之后还有一个线程管理部分也是影响性能的关键组件同时.NET还提供了一个由数千个API组成的运行库这些类型中无论是基元类型还是泛型集合类型都获得了若干提升这些部分共同组成了这几个版本的性能奇迹。
本篇文章中的主要内容来自于.NET运行时仓库中的[Book of the Runtime](https://github.com/dotnet/runtime/blob/main/docs/design/coreclr/botr/README.md)和微软开发者博客上的[Performance Improvements in .NET 6](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/)、[Performance Improvements in .NET 7](https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/)和[Performance Improvements in .NET 8](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/#whats-next)等几篇文章,上述没有覆盖到的内容推荐读者这些文章。同时算算时间,.NET 9版本引入的性能提升文章应该也要发布了。
回到文章最开始时的问题JIT编译就一定比AOT编译慢吗从启动速度上来说JIT编译当然是完败AOT编译但是在程序长时间运行各项设备JIT编译器、运行时和GC等预热完成之后则是鹿死谁手犹未可知了。

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,316 @@
---
title: 环境配置备忘录
date: 2022-01-15T20:19:39.0000000
tags:
- 技术笔记
---
电脑上的环境三天两头出问题,写下一个备忘录记录一下电脑上环境的配置过程。
<!--more-->
> Update1: 2022年9月4日
>
> 重新配置了一遍电脑中的环境,删除了许多不切实际的地方。
## 操作系统
版本Windows10专业版
版本号21H2
操作系统内部版本19044.1949
体验Windows Feature Experience Pack 120.2212.4180.0
> 虽然不知道上面的有什么用,但是还是写一下,没准什么时候就有用了。
## Windows Subsystem for Linux(WSL)
安装WSL的官方文档[链接](https://docs.microsoft.com/zh-cn/windows/wsl/install) 。
输入`wsl -l -v`输出
```
NAME STATE VERSION
* Ubuntu Stopped 2
```
使用的Linux分发版是Ubuntu使用的WSL版本是WSL-2。
## 语言
### Python
前往下载安装Python。
使用下列命令将pip下载换源为TUNA源
```bash
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
```
### Java
在[这里](https://www.oracle.com/java/technologies/downloads/)下载JDK ,然后配置相关的环境变量 首先设置JAVA_HOME指向安装JDK的根路径。
然后是CALSSPATH内容是
```
.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar;
```
再设置PATH相关变量
```
%JAVA_HOME%\bin;
%JAVA_HOME%\jre\bin
```
在PowerShell中输入
```
java --version
```
返回
```
java 17.0.4.1 2022-08-18 LTS
Java(TM) SE Runtime Environment (build 17.0.4.1+1-LTS-2)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.4.1+1-LTS-2, mixed mode, sharing)
```
输入
```
javac --version
```
返回
```
javac 17.0.4.1
```
确认相关的设置完成 。
#### JAVA_TOOL_OPTIONS
设置一个环境变量`JAVA_TOOL_OPTIONS`
```
-Dfile.encoding=UTF8 -Duser.language=en
```
从而规避中文乱码的问题。
> 其他一些官方文档较为全面,安装不需要复杂配置的编程语言就不再赘述了
## IDE
### VSCode
在[这里](https://code.visualstudio.com/)下载。
#### C/C++时的配置文件
```json
// launch.json
{
"version": "0.2.0",
"configurations": [
{//这个大括号里是我们的‘调试(Debug)’配置
"name": "Debug",//配置名称
"type": "cppdbg",//配置类型cppdbg对应cpptools提供的调试功能可以认为此处只能是cppdbg
"request": "launch",// 请求配置类型可以为launch启动或attach附加
"program": "${workspaceFolder}/bin/${fileBasenameNoExtension}.out",// 将要进行调试的程序的路径
"args": [],// 程序调试时传递给程序的命令行参数,这里设为空即可
"stopAtEntry": false,// 设为true时程序将暂停在程序入口处相当于在main上打断点
"cwd": "${workspaceFolder}/bin/",//程序的工作目录
"environment": [],//环境变量,设置为空
"externalConsole": false,// 为true时使用单独的cmd窗口跳出小黑框设为false则是用vscode的内置终端建议用内置终端
"internalConsoleOptions": "neverOpen",// 如果不设为neverOpen调试时会跳到“调试控制台”选项卡新手调试用不到
"MIMode": "gdb",//指定特定的调试器
"miDebuggerPath": "/usr/bin/gdb",//指定的调试器所在路径
"preLaunchTask": "build"// 调试开始前执行的任务我们在调试前要编译构建。与tasks.json的label相对应名字要一样
}
]
}
```
```json
// setting.json
{
"files.associations": {
"stdio.h": "c",
"xutility": "c",
"stdlib.h": "c",
"math.h": "c",
"cmath": "c"
},
"C_Cpp.errorSquiggles": "EnabledIfIncludesResolve"
}
```
```json
// tasks.json
{
"version": "2.0.0",
"tasks": [
{//这个大括号里是构建的配置文件
"label": "build",//任务的名称
"type" : "shell",//任务类型process是vsc把预定义变量和转义解析后直接全部传给commandshell相当于先打开shell再输入命令所以args还会经过shell再解析一遍
"command": "gcc",//在shell中执行的命令若编译C++改为g++
"args": [//一些传递给命令的参数
"${file}",
"-o",
"${workspaceFolder}/bin/${fileBasenameNoExtension}.out",//这里是生成exe程序的位置因为我自己设置了bin文件夹的位置因此我直接使用绝对路径
"-g",//生成和调试有关的信息
"-Wall",//开启额外警告
"-static-libgcc",//静态链接libgcc
"-lm",//链接一个库文件
"-std=c11", // 语言标准可根据自己的需要进行修改写c++要换成c++的语言标准比如c++11
],
"group": {
"kind": "build",//表示这一组任务类型是构建
"isDefault": true//表示这个任务是当前这组任务中的默认任务
},
"presentation": {
"echo": true,//表示在执行任务时在终端要有输出
"reveal": "always",//执行任务时是否跳转到终端面板可以为alwayssilentnever
"focus": false,//设为true后可以使执行task时焦点聚集在终端但对编译来说设为true没有意义因为运行的时候才涉及到输入
"panel": "new",//每次执行这个task时都新建一个终端面板也可以设置为shared共用一个面板不过那样会出现任务将被终端重用的提示比较烦人
"showReuseMessage": true,
"clear": false
},
"problemMatcher":"$gcc",////捕捉编译时编译器在终端里显示的报错信息将其显示在vscode的问题面板里
},
{
"label": "run",
"type": "shell",
"dependsOn":"build",
"command":"${workspaceFolder}/bin/${fileBasenameNoExtension}.out",//这里是运行生成的程序的命令,同样使用绝对路径
"group": {
"kind": "test",
"isDefault": true,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": true,////这个就设置为true了运行任务后将焦点聚集到终端方便进行输入
"panel": "new",
"showReuseMessage": true,
"clear": false
}
}
]
}
```
### Jetbrians
使用`Jetbrains Toolbox`来管理电脑上的所有IDE。
## 终端美化
首先在微软应用商店下载安装`Windows Terminal`
### PowerShell美化
前往[oh-my-posh](https://github.com/jandedobbeleer/oh-my-posh)下载这个Powershell的美化工具将安装的位置放进`PATH`环境变量中,在终端中输入
```powershell
oh-my-posh --version
```
确认是否安装成功。
同时将下载下来的主题文件放在一个特定的地方(我这里是`C:\Users\ricardo\Programs\oh-my-posh\themes`在PowerShell的启动配置文件`$PROFILE`中加入下面几句
```powershell
Set-Item env:POSH_THEMES_PATH "C:\Users\ricardo\Programs\oh-my-posh\themes"
oh-my-posh init pwsh --config "$env:POSH_THEMES_PATH/paradox.omp.json" | Invoke-Expression
```
下载[posh-git](https://github.com/dahlbyk/posh-git)这是一个给PowerShell提供Git相关辅助的模块下载完成之后将下列命令添加到`$PROFILE`
```powershell
Import-Module ~\Programs\posh-git\src\posh-git.psd1
```
如果在修改了配置文件之后启动提示运行脚本没有经过签名,可以采用下面这条命令来修改运行脚本的权限:
```powershell
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser
```
再重新运行配置文件就没有问题了。
![终端预览](6.webp)
### PowerShell配置文件
```powershell
# oh-my-posh setup
oh-my-posh init pwsh --config "C:\Users\ricardo\Programs\oh-my-posh\themes\paradox.omp.json" | Invoke-Expression
Import-Module ~\Programs\posh-git\src\posh-git.psd1
# proxy functions
function Set-Proxy {
Set-Item Env:http_proxy "http://127.0.0.1:7890"
Set-Item Env:https_proxy "http://127.0.0.1:7890"
}
function Remove-Proxy {
Remove-Item Env:http_proxy
Remove-Item Env:https_proxy
}
```
### WSL美化
目前我还在使用`bash`,不过使用`oh-my-posh`进行了一定的美化。
同Windows下一致进行`oh-my-posh`的下载和安装,在`~/.bashrc`中添加这样一句配置文件
```bash
eval "$(oh-my-posh init bash --config /home/ricardo/.poshthemes/paradox.omp.json)"
```
### BASH设置
`~/.bashrc`文件的末尾新增
```bash
# oh-my-posh setup
eval "$(oh-my-posh init bash --config /home/ricardo/.poshthemes/paradox.omp.json)"
# proxy function
export hostip=$(cat /etc/resolv.conf | grep "nameserver" | cut -f 2 -d " ")
proxy()
{
export http_proxy = "http://${hostip}:7890"
export https_proxy = "http://${hostip}:7890"
}
unproxy()
{
export http_proxy=""
export https_proxy=""
}
# alias settings
alias python=python3
```
## 其他的小工具
字体:
- [Fira Code](https://github.com/tonsky/FiraCode)
命令行工具
- [dust](https://github.com/bootandy/dust)
- [scc](https://github.com/boyter/scc)
- [tcping](https://github.com/cloverstd/tcping)

BIN
source/posts/environment-setting/6.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,88 @@
---
title: 原神抽卡研究一
date: 2022-12-31T13:38:19.0000000
tags:
- 原神
- 学习资料
---
实际上是“概率论和随机过程”课程的期末小论文。
<!--more-->
## 背景
目前在市面上出现了大量以抽奖为核心盈利手段的电子游戏,在这种游戏中,获取游戏中的物品不是明码标价的购买,而是通过参加某种抽奖性质的活动。玩家花费一定金额购买参加活动的机会,每次参加都有一定的概率获得玩家想要获得的物品。在抽奖活动中,游戏设计者还会引入一种被称为 “保底” 的游戏机制:开发者向玩家群体承诺在一定的参加次数之后必然会获得到该物品。例如下面是热门游戏《原神》中获取游戏中角色 “抽奖” 活动的概率公示页面。
![](./genshin-gacha-1/2022-12-31-13-06-36-image.webp)
在这个 “抽奖” 活动中,玩家可以得到三种等级的物品,在游戏中分别称为 “五星物品”、“四星物品” 和 “三星物品”。在每次的 “抽奖” 活动中,玩家必定会获得上述三种物品中的一种。为了简化问题的讨论,我们现不区分相同等级不同物品之间的不同,只考虑不同星级物品的获取概率。通过概率公示可以知道:五星物品的 “基础概率” 为0.600%,四星物品的 “基础概率” 为 5.100%;五星物品的 “综合概率” 为 1.600%,四星物品的 “综合概率” 为 13.000%。从一个玩家的角度出发,自然会存在两个问题:
- 什么是“基础概率”,什么是“综合概率”?
- 游戏中的实际概率和公式概率吻合吗?
## 研究方法
在大量的重复实验中,事件 A 发生的频率往往具有稳定性,随着重复实验次数的增加,事件发生的频率逐渐稳定于某个固定的客观的常数。由于大数定理,频率最终会收敛到概率。因此,可以通过大量玩家参与这个 “实验” 的数据来回答上述的问题。
### 数据的获取
一般而言,游玩游戏的玩家只能看见自己过往参加游戏时的记录,但是在一些第三方平台上允许其他玩家主动上传数据并公示给大家。虽然这些第三方平台上数据不能完全保证可靠性,可以通过统计中筛选删除明显错误和容易对结果产生较大影响的数据,即使仍然存在少量的错误数据也不会对结果产生比较明显的影响。这里采用[OneBST](https://github.com/OneBST)从第三方平台 [“非小酋” 网站](https://feixiaoqiu.com)获取的截至 2022 年 7 月 26 日的数据作为数据集。
> 数据可以在[Github仓库](https://github.com/OneBST/GI_gacha_dataset/)中下载。
### 数据的统计和分析
利用 Python 中的 Numpy 和 Pandas 等工具包对获取到的数据进行统计分析,在对获取到的数据进行分析的过程中按照以下的规则排除明显错误和对结果影响比较大的数据:
- 抽卡中多次出现违反“保底”规则的数据例如多次获得“四星”物品的间隔超过10次。
- 数据中应该唯一的字段“gacha_id"大量重复。
- 第一次获得四星或者五星物品。
- 数据本身的错误。
## 结论
### 综合概率的解释
在总共 4842256 次抽卡记录中,获得五星物品的次数为 78493 次,于是:
![](genshin-gacha-1/2022-12-31-15-59-20-image.webp)
再计算一下每次参加该活动获得四星物品的平均概率:在总共 5000139 次抽卡中,获得四星物品的次数为 653200
![](genshin-gacha-1/2022-12-31-15-59-42-image.webp)
不难发现,在误差允许的范围内,计算出来的平均概率和游戏开发者所公布的 “综合概率” 是相同的。当参与这个游戏足够多次时,获取到五星物品和四星物品的数量就可以用这个概率来估计。
### 抽卡过程的数学描述
为了方便讨论,再次将这个 “游戏” 简化为获得五星物品和不获得五星物品两种情况。那么这个 “抽奖” 游戏是否就能被简化为一个概率为 1.6% 的 n 次伯努利实验?不妨假设每次获得五星物品之间相互独立,这样每次获得五星物品都可以认为是首次获得五星物品,这时参加该游戏的次数就会符合概率为 1.6% 的几何分布,而为了符合保底规则,当玩家在参与到第 90 次时仍未获得五星物品,强制给予玩家一个五星的物品。画出实际数据中得到的图像和按照几何分布得到的图像。
![](genshin-gacha-1/2022-12-31-13-20-46-image.webp)
![](genshin-gacha-1/2022-12-31-13-21-11-image.webp)
不难发现假设的猜想和实际情况不相符合。在抽数小于 73 抽时,获得五星物品的概率逐渐降低,从 0.6% 左右一直降低至 0.4% 左右。当抽数大于等于 73 抽时,抽到的概率开始上升,在抽数等于 77 抽时达到最大,大约为 10.4%。随后概率开始下降,在第 91 抽时,概率等于 0。
如果在实际得到的概率关于抽数的图上再作出概率为 0.6% 的几何分布的图像前73 抽的概率图像和几何分布的图像几乎吻合。也就是说,该抽奖游戏的前 73 抽就是一个符合 P = 0.6% 的几何分布,从第 73 抽开始 “保底” 机制的修正。这就是游戏开发者口中 “基础概率” 的含义:在该抽奖游戏的前数十抽就是一个概率为 0.6% 的伯努利实验。
![](genshin-gacha-1/2022-12-31-13-24-26-image.webp)
从大量的实际数据出发,不难发现游戏开发者的申明同实际情况相吻合。
## 不足和展望
### 获取五星物品相互独立的假设
直到作者开始写作本文之前,作者都没有意识到本文 3.2 节中的所有结论几乎都基于该假设。限于文章的篇幅原因和个人的能力问题,在本文中未对这个假设作出验证。下面给出一种验证该猜想的方法。为验证获得五星物品之间相互独立,可以通过统计方法验证下面的等式成立。
![](genshin-gacha-1/2022-12-31-16-00-10-image.webp)
### 展望
在获得概率和抽数之间的相关关系之后,可以将这个抽卡的过程当作一个随机过程进行析,并且求出相关的数学特征。从玩家的角度出发,可以指导玩家使用比较合理的策略参加这个游戏,从而以比较低的代价获得自己心仪的物品。从游戏开发者的角度出发,则有有利于优化自己游戏中的概率设计,吸引更多的玩家参与到游戏中来。

BIN
source/posts/genshin-gacha-1/2022-12-31-13-06-36-image.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/genshin-gacha-1/2022-12-31-13-20-46-image.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/genshin-gacha-1/2022-12-31-13-21-11-image.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/genshin-gacha-1/2022-12-31-13-24-26-image.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/genshin-gacha-1/2022-12-31-15-59-20-image.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/genshin-gacha-1/2022-12-31-15-59-42-image.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/genshin-gacha-1/2022-12-31-16-00-10-image.webp (Stored with Git LFS) Normal file

Binary file not shown.

View File

@@ -0,0 +1,876 @@
---
title: 异构编程模型的昨天、今天与明天
date: 2024-11-04T22:20:41.2571467+08:00
tags:
- 编译原理
- 组会汇报
---
随着摩尔定律的逐渐失效将CPU和其他架构的计算设备集成在片上或者通过高速总线互联构建的异构系统成为了高性能计算的主流。但是在这种系统中上层应用的设计与实现面临着异构系统中各个设备之间体系结构差异过大、缺乏良好的异构抽象以及统一的编程接口和应用程序的优化难度大等困难。
异构并行编程模型便是解决这些编程和执行效率问题的解决方案。
<!--more-->
## 异构并行编程模型概述
异构并行编程模型是沟通上层应用和下层异构系统之间的桥梁,其的设计需要处理好下面五个问题:任务划分、任务映射、数据分布、同步和通信。
### 异构并行编程模型面临的技术挑战
异构并行编程模型面临的技术挑战主要是由两方面带来的:首先异构架构本身为编程模型带来的挑战,其次是上层应用带来的挑战。
异构并行编程模型需要解决的一个重要问题就是为上层应用的程序员提供一个合理的硬件平台抽象,使得其在编程是可以充分释放异构资源带来的计算能力,同时不需要考虑复杂的硬件细节。但是异构系统中各个计算设备在内部体系结构、设备间互联架构上的复杂性和多样性使得异构并行编程模型在提供建立统一的平台抽象上遇到了巨大的困难。具体来说,主要体现下述三点。
首先是异构系统中各个设备之间的并行计算能力不同。在同构的并行计算系统中比如多核CPU中虽然同一CPU的不同核之间、同一核的不同SIMD部件之间可以承担不同粒度的并行计算任务但是其并行计算的能力是完全相同的。但是在一个典型的异构计算系统例如CPU、GPU和FPGA组成的异构系统不同设备的微架构具有本质差异其并行计算的模式和能力都完全不同设备之间的特长也完全不同。这种设备之间并行计算能力的差异使得系统中的任务划分和任务映射不再是均一的而是具有显著的特异性。这种特点虽然也有利于表达实际应用的特点但是却给异构并行计算模型的设计带来了巨大的困难。
![9eb06d8be92ddef3db33e040163c67a7.webp](./heterogeneous-programming-model/9eb06d8be92ddef3db33e040163c67a7.webp)
其次是异构系统中加速设备数据分布可配置、设备间数据通信渠道多样性给数据分布和通信带来的困难。在同构并行系统中CPU片内的存储是对于软件透明的缓存架构在片外则是一个共享内存模型因此在这类系统中数据仅可能分布在片外的共享存储中具有存储位置单一的特点也不需要进行显式的通信操作。但是在异构系统中不仅在单个加速设备内部可能有软件可分配的快速局部存储设备之间的连接方式差异也很大。目前大多个加速设备都是通过PCIe总线的方式同CPU进行连接这使得加速设备无法通过和CPU相同的方式完成地址映射存在某一设备无法访问另一设备片外存储的问题。这使得异构系统中数据可以分布在CPU、加速设备的片外存储和加速设备的片内多层次局部存储等多个位置不仅使得编程模型的数据分布问题变得十分复杂设备间的通信文件也可能需要显式进行。
![eab553f9e30d8d866a1ddd201b5e4c85.webp](./heterogeneous-programming-model/eab553f9e30d8d866a1ddd201b5e4c85.webp)
最后是异构系统中多层次数据共享和多范围同步操作带来的同步困难问题。这也可以认为是上个数据同步问题带来的后继问题在异构系统中数据可能分布在不同位置的条件下同步操作需要在众多的位置上保证共享数据的一致性这使得同步操作的范围变得十分复杂。同时在一些特定的加速设备中例如GPU可能还会有局部的硬件同步机制这更加提高了在异构系统的同步操作的设计和实现难度。
上层应用带来的挑战主要集中在缺少良好的异构抽象和统一的编程接口上。例如在CPU上进行编程时通常使用Java、Python等高级语言而在进行GPU编程时则使用各种C语言的变体其中的核心计算函数Kernel Function则通常只支持一个C语言的子集而FPGA这些硬件设备又需要使用硬件描述语言进行编程。
### 异构并行编程接口和编译/运行时支持机制
异构并行编程接口是编程模型暴露给程序员使用的界面,它既需要为程序员提供合理的异构架构抽象,使程序员可以对异构计算资源加以合理利用,又需要保证接口的易用性,避免程序员陷入复杂的硬件细节中。编译/运行时系统是异构并行编程模型的软件工具层,它将程序员编写的加速器代码编译为可执行文件,并通过运行时系统完成任务的加速执行。
在任务划分、任务映射、数据分布、通信和同步这五个关键任务中,程序员往往只需要关注所编写应用程序的特点,因此显示的任务划分机制对应程序员来说可能是必不可少的,而其他的数据分布、通信和同步等任务只会加剧程序员开发应用程序的负担,但是这些任务通过接口暴露出来也为后续进行深度优化提供了空间。异构编译/运行时支持机制的主要任务就是保障任务映射,即明确任务将具体在哪个设备或者计算单元上执行,以何种顺序执行,同时在当程序员没有显式处理数据分布、通信和同步问题时进行自动处理并进行全系统级别的优化。
## 异构并行编程接口的研究
异构并行编程接口一般可以划分成两类新设计的异构编程语言和现有语言的异构并行扩展。对于现有语言进行的异构并行扩展一般通过库Library或者是制导Directive的方法进行。
从异构并行编程接口的功能角度上来说也可以分成两类:有些接口屏蔽了较多的异构并行编程细节,通常仅给程序员提供显式异构任务划分的机制,而数据分布和通信、同步等的工作由运行时系统负责完成,也有些接口将多数异构系统的硬件细节通过上述机制暴露给程序员使用,这在给编程带来更大自由度的同时带来了使用上的困难。
![83ee1d254d638536d0fb4197ff63e758.webp](./heterogeneous-programming-model/83ee1d254d638536d0fb4197ff63e758.webp)
### 异构任务划分机制研究
在同构的并行编程语言中,并行编程接口需要提供一种面向单一设备的并行任务划分机制,这种并行任务划分机制有**任务并行**和**数据并行**等。数据并行指的是对源集合或者数组的元素同时执行相同操作的场景,一个数据并行的典型例子如下面计算两个矩阵的乘积:
```csharp
static void MultiplyMatricesParallel(double[,] matA, double[,] matB, double[,] result)
{
int matACols = matA.GetLength(1);
int matBCols = matB.GetLength(1);
int matARows = matA.GetLength(0);
// A basic matrix multiplication.
// Parallelize the outer loop to partition the source array by rows.
Parallel.For(0, matARows, i =>
{
for (int j = 0; j < matBCols; j++)
{
double temp = 0;
for (int k = 0; k < matACols; k++)
{
temp += matA[i, k] * matB[k, j];
}
result[i, j] = temp;
}
}); // Parallel.For
}
```
任务并行的概念一般是指一个或者多个独立的任务同时运行,是一种比数据并行更高的抽象层级。
```csharp
public class Result
{
public static void Main()
{
Task<Double>[] taskArray = { Task<Double>.Factory.StartNew(() => DoComputation(1.0)),
Task<Double>.Factory.StartNew(() => DoComputation(100.0)),
Task<Double>.Factory.StartNew(() => DoComputation(1000.0)) };
var results = new Double[taskArray.Length];
Double sum = 0;
for (int i = 0; i < taskArray.Length; i++) {
results[i] = taskArray[i].Result;
Console.Write("{0:N1} {1}", results[i],
i == taskArray.Length - 1 ? "= " : "+ ");
sum += results[i];
}
Console.WriteLine("{0:N1}", sum);
}
private static Double DoComputation(Double start)
{
Double sum = 0;
for (var value = start; value <= start + 10; value += .1)
sum += value;
return sum;
}
}
```
不论是高级或者是低级的异构并行编程接口都需要提供一种异构并行任务的划分机制。同传统的同构并行编程接口只需要提供面向单一设备的并行任务划分机制不同,异构并行编程接口还需要提供描述任务在不同设备间分配的机制。因此,异构并行编程接口的任务划分机制需要包括两个维度:异构特征描述和并行性表达两个维度。
一种典型异构任务划分机制是由`BrookGPU`编程语言提出的。该编程语言采用特殊函数`kernel`标记需要在GPU上执行的代码段`kernel`函数必须作用在流上。这个流Stream在并行性表达方面表达了细粒度的数据并行。后面的OpenCL和CUDA在C语言的基础上提供了异构扩展这种扩展的任务划分机制和`BrookGPU`的十分类似。但是OpenCL和CUDA在并行行表达的层面上支持了SPMD计算模型这个`BrookGPU`编程语言采用的流式编程模型不同。OpenCL在数据并行之外还提供了任务并行的机制。
`Lime`则是一门完全新的异构并行编程语言,通过语言结构为程序提供了丰富的操作符用于任务的划分。同时在异构特征描述方面,`Lime`也没有任何显式的接口,同`BrookGPU`等一系列需要手动指定设备代码段的编程模型完全不同,这也是因为`Lime`采用了基于任务的并行划分方式。同时在任务并行之外,`Lime`也通过`MapReduce`操作符提供了中粒度的数据并行机制。
`Merge`还是一门新的异构并行编程语言基于Intel提出的异构多核多线程系统编程环境`EXOCHI`。在并行性表达上,`Merge`使用`MapReduce`思想。而在异构特征描述方面,`Merge`则提供了一种成为平台变体Target Variant的机制程序员需要为异构系统中的不同设备提供不同版本的代码实现。
### 异构数据分布和通信机制
异构数据分布和通信机制主要分成显式和隐式两种,其中`OpenCL/CUDA`等使用了显式的数据分布的通信机制,为程序员提供了丰富的异构数据分布与通信接口。而`Lime``Merge`等语言则使用了隐式机制,运行时系统代为完成这部分的工作。
采用显示异步数据分布和通信机制的主要问题是普通程序员一般无法充分利用这些接口获得性能上的提升。这通常使用因为加速设备通常采用了大量的硬件加速机制例如GPU的全局内存访存合并机制这使得程序员如果没有为数据分配合理的存储位置或者设定足够多的线程会使得加速的效果大打折扣。因此出现了针对这类显式控制语言的优化方法例如`CUDA-lite`这个运行时允许程序元在CUDA程序中加入简单的制导语句数据分布的相关工作使用`CUDA-lite`的运行时系统完成降低了CUDA程序的编写难度。
![628804b3fe95d39013ff55ae84516d14.png](./heterogeneous-programming-model/Screenshot_20241016_214139.webp)
总结一下,为了解决异构系统带来的问题,异构并行编程接口具有如下三个特点:
- 异构任务划分机制在传统并行编程模型的基础上增加了"异构特征描述"的维度,用于描述任务在不同设备上的分配情况;
- 异构数据分布和通知机制在传统并行编程模型的基础上增加了"设备内数据多层分布"和"设备间显式通信"接口;
- 异构同步机制在传统并行编程模型的基础上增加了"设备间同步"的机制。
## 异步编译/运行时的研究
### 异构任务映射机制
异构编程/运行时系统的任务映射机制主要有两种:一类是直接映射,即独立完成并行任务向异构平台映射的工作,另一种是间接映射,即需要借助其他异构编译和运行时系统协助来完成部分任务映射工作。直接映射系统一般在运行时系统中实现,而间接映射通过源到源变换和是运行时分析相结合的方式实现。
![](./heterogeneous-programming-model/Screenshot_20241016_214939.webp)
### 异构编译/运行时优化
与同构平台类似,异构编译/运行时优化有两条路径:
- 平台相关的优化,其核心是挖掘系统的硬件优势;
- 应用导向的优化,其核心是实施特定领域的优化并解决应用的输入敏感问题。
在平台优化上,异构系统通常具有复杂且多变的硬件结构, 因此程序员仅负责编写正确实现程序功能的代码、由编译/运行时系统完成面向加速设备结构特点的优化是比较合理的方式, 这样也有利于程序在不同异构系统中获得良好的性能。
## 异构并行编程模型的研究方向
- 面向普通用户的异构并行编程接口
- 面向多种加速设备的异构编译/运行时优化
- 面向异构集群的异构并行编程模型
## 异构并行编程模型调研
为了调研各个异构并行编程模型的不同,使用不同的编程模型实现一个通用矩阵乘法算法,并通过计算`2048*2048`大小的矩阵乘法时间来比较各个模型的加速效果。
辅助计算的`Calculator`类如下所示:
```cpp
#define MATRIX_SIZE 2048
#include <chrono>
#include <functional>
#include <iostream>
#include <random>
class Calculator
{
public:
static void validate_matrix(const std::vector<std::vector<int>>& a, const std::vector<std::vector<int>>& b)
{
for (int i = 0; i < MATRIX_SIZE; i++)
{
for (int j = 0; j < MATRIX_SIZE; j++)
{
if (a[i][j] != b[i][j])
{
std::cout << "Two matrix must be the same." << std::endl;
}
}
}
}
std::vector<std::vector<int>> calculate(const std::string& method,
const std::function<std::vector<std::vector<int>>(
const std::vector<std::vector<int>>&,
const std::vector<std::vector<int>>&)>& calculator) const
{
std::cout << "Calculator '" << method << "' start." << std::endl;
const auto start_time = std::chrono::high_resolution_clock::now();
const auto result = calculator(a, b);
const auto end_time = std::chrono::high_resolution_clock::now();
const auto span = end_time - start_time;
std::cout << "Calculator '" << method << "' end, time is " << std::chrono::duration_cast<
std::chrono::milliseconds>(span).count() << " ms." << std::endl;
return result;
}
private:
std::vector<std::vector<int>> a = initialize_matrix();
std::vector<std::vector<int>> b = initialize_matrix();
static std::vector<std::vector<int>> initialize_matrix()
{
std::vector<std::vector<int>> matrix;
std::random_device seed;
std::ranlux48 engine(seed());
std::uniform_int_distribution distribute(0, 102400);
for (int i = 0; i < MATRIX_SIZE; i++)
{
std::vector row(MATRIX_SIZE, 0);
for (int j = 0; j < MATRIX_SIZE; j++)
{
row[j] = distribute(engine);
}
matrix.emplace_back(row);
}
return matrix;
}
}
```
作为对比一个使用CPU单线程计算的例子如下
```cpp
inline std::vector<int> cpuMatrixMultiply(
const std::vector<int>& a,
const std::vector<int>& b)
{
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
for (int i = 0; i < MATRIX_SIZE; i++)
{
for (int j = 0; j < MATRIX_SIZE; j++)
{
int temp = 0;
for (int k = 0; k < MATRIX_SIZE; k++)
{
// a[i][j] = a[i][k] * b[k][j] where k in (0..MATRIX_SIZE)
temp += a[i * MATRIX_SIZE + k] * b[k * MATRIX_SIZE + j];
}
result[i * MATRIX_SIZE + j] = temp;
}
}
return result;
}
```
### OpenMP
OpenMP是`Open MultiProcessing`的缩写是一个使用编译器制导Directives来进行共享内存平行计算的框架在C、C++和Fortran语言的并行编程中得到的了广泛的应用。OpenMP提供了一个简单而灵活的接口让程序员能够充分释放多核和多处理器系统性能。
OpenMP从上面的介绍来看似乎并不是一个严格的异步并行编程模型但是第一OpenMP作为一个经典的并行编程框架研究价值还是非常高的其次在一些较新的OpenMP版本中其宣称也能利用NVIDIA GPU进行加速似乎也能算是一个异构并行编程模型。
使用OpenMP进行并行加速的代码如下
```C++
std::vector<std::vector<int>> omp_matrix_multiply(
const std::vector<std::vector<int>>& a,
const std::vector<std::vector<int>>& b)
{
std::vector result(MATRIX_SIZE, std::vector(MATRIX_SIZE, 0));
#pragma omp parallel for shared(a, b, result) default(none)
for (int i = 0; i < MATRIX_SIZE; i++)
{
for (int j = 0; j < MATRIX_SIZE; j++)
{
int temp = 0;
for (int k = 0; k < MATRIX_SIZE; k++)
{
temp += a[i][k] * b[k][j];
}
result[i][j] = temp;
}
}
return result;
}
```
加速的结果如下:
| 运行方法 | 运行时间 | 比率 |
| ------------ | -------- | ---- |
| SingleThread | 21685 ms | 1.00 |
| OpenMP | 2268 ms | 0.10 |
### CUDA
CUDA是NVIDIA公司设计的一套GPU加速应用程序的编程框架是将NVIDIA GPU作为GPGPU使用的官方解决方案。
CUDA的异构编程接口是经典的Device-Host两元结构程序员需要编写两部分代码Device代码是实际运行在GPU上的逻辑部分而Host代码则负责将数据从内存中复制到GPU上的显存和复制回来等准备工作并负责以特定的参数调用GPU上的Device代码。
一个使用GPU的矩阵乘法程序如下所示
```c++
template <typename T>
void check(T result, char const* const func, const char* const file, int const line)
{
if (result)
{
std::cerr << "CUDA error at " << file << ":" << line << "code = " << result << "(" << cudaGetErrorString(result)
<< ") '" << func << "'" << std::endl;
exit(EXIT_FAILURE);
}
}
#define checkCudaErrors(val) check((val), #val, __FILE__, __LINE__)
__global__ void cudaMatrixMultiply(const int* a, const int* b, int* c)
{
const int totalSize = MATRIX_SIZE * MATRIX_SIZE;
int threadId = threadIdx.x + blockIdx.x * blockDim.x;
while (threadId < totalSize)
{
const int x = threadId / MATRIX_SIZE;
const int y = threadId % MATRIX_SIZE;
int result = 0;
for (int i = 0; i < MATRIX_SIZE; i++)
{
result += a[x * MATRIX_SIZE + i] * b[i * MATRIX_SIZE + y];
}
c[MATRIX_SIZE * x + y] = result;
threadId += gridDim.x * blockDim.x;
}
__syncthreads();
}
std::vector<std::vector<int>> cudaCalculateMatrix(const std::vector<std::vector<int>>& a,
const std::vector<std::vector<int>>& b)
{
constexpr unsigned int matrixSize = sizeof(int) * MATRIX_SIZE * MATRIX_SIZE;
// 在host上为a, b, c分配空间
int *hostA, *hostB, *hostC;
checkCudaErrors(cudaMallocHost(&hostA, matrixSize));
checkCudaErrors(cudaMallocHost(&hostB, matrixSize));
checkCudaErrors(cudaMallocHost(&hostC, matrixSize));
// 将数据复制到host上
for (int i = 0; i < MATRIX_SIZE; i++)
{
for (int j = 0; j < MATRIX_SIZE; j++)
{
hostA[i * MATRIX_SIZE + j] = a[i][j];
hostB[i * MATRIX_SIZE + j] = b[i][j];
}
}
// 在device上分配空间
int *deviceA, *deviceB, *deviceC;
checkCudaErrors(cudaMalloc(reinterpret_cast<void**>(&deviceA), matrixSize));
checkCudaErrors(cudaMalloc(reinterpret_cast<void**>(&deviceB), matrixSize));
checkCudaErrors(cudaMalloc(reinterpret_cast<void**>(&deviceC), matrixSize));
cudaStream_t stream;
checkCudaErrors(cudaStreamCreateWithFlags(&stream, cudaStreamNonBlocking));
// 将数据从host复制到device
checkCudaErrors(cudaMemcpyAsync(deviceA, hostA, matrixSize, cudaMemcpyHostToDevice, stream));
checkCudaErrors(cudaMemcpyAsync(deviceB, hostB, matrixSize, cudaMemcpyHostToDevice, stream));
constexpr int threadSize = 32 * 32;
constexpr int grid = MATRIX_SIZE * MATRIX_SIZE / threadSize;
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
cudaStreamSynchronize(stream);
cudaEventRecord(start, stream);
cudaMatrixMultiply<<<grid, threadSize, 0, stream>>>(deviceA, deviceB, deviceC);
cudaEventRecord(stop, stream);
cudaEventSynchronize(stop);
float cudaRunTime = 0;
cudaEventElapsedTime(&cudaRunTime, start, stop);
std::cout << "CUDA actual run time is " << cudaRunTime << " ms" << std::endl;
// 将数据从device复制到host
cudaMemcpyAsync(hostC, deviceC, matrixSize, cudaMemcpyDeviceToHost, stream);
cudaStreamSynchronize(stream);
std::vector<std::vector<int>> result;
for (int i = 0; i < MATRIX_SIZE; i++)
{
std::vector<int> row;
for (int j = 0; j < MATRIX_SIZE; j++)
{
row.emplace_back(hostC[i * MATRIX_SIZE + j]);
}
result.emplace_back(row);
}
// 释放内存
cudaFreeHost(hostA);
cudaFreeHost(hostB);
cudaFreeHost(hostC);
cudaFree(deviceA);
cudaFree(deviceB);
cudaFree(deviceC);
cudaEventDestroy(start);
cudaEventDestroy(stop);
cudaStreamDestroy(stream);
return result;
}
```
加速的结果如下所示:
| 类型 | 运行时间 | 比率 |
| ---- | -------- | ----- |
| CPU | 22059ms | 1.000 |
| GPU | 32ms | 0.001 |
需要注意的是上面编写的CUDA代码还没有完全利用GPU的并行计算能力。
> 这里我遇到的一个非常奇怪的问题是相同的CPU计算代码在运行完OpenMP测试之后再运行就会比在CUDA运行之后再运行慢上一倍而且可复现性极高。这里我给出一个典型的运行时间比较CUDA计算的时间是323毫秒CUDA之后的CPU计算时间是38602毫秒OpenMP的计算时间是8721毫秒OpenMP之后的计算时间是76598毫秒。
>
> 针对这个比较奇怪的情况我觉得可以做出三个猜想:
>
> - 考虑到我使用的CPU是Intel的i7-13600K这是一个有性能核和效率核组成的大小核异构系统可能在两次计算的过程中调度到了不同的核上
> - 在进行CUDA计算的过程中提高了缓存的亲和性
> - 在测试中没有设计热身Warm up的过程而在CUDA计算的过程中部分起到了这个作用。
>
> 针对上面三个猜测做个两个实验:
>
> - 首先是换了一台没有大小核异构设计的计算机进行实验发现这下两次使用CPU计算的时间差异不大
> - 加上了热身的阶段之后,计算时间没有发生明显的变化。
>
> 综上所述可以认为此现象和异构CPU之间存在着明显的关联但是缺乏直接证据。
>
> 在我们调整了矩阵的数据布局之后这里提到的实验结果又发生了变化。上面的实验结果是使用二维数据存储矩阵得到的而在修改为使用一维数组也就是现在提供的代码之后相同的CPU计算代码的计算时间又没有产生明显的变化了。看来这个问题可能和数据布局、CPU缓存等问题相关。
### OpenCL
OpenCL是目前最为典型、发展最好的异构并行编程模型毕竟其在官网的第一句话就是“为异构系统中并行编程的开放标准“。
![image-20241020142938110](./heterogeneous-programming-model/image-20241020142938110.webp)
从上图的OpenCL工作原理中可以看出OpenCL和CUDA类似也采用了Device-Host类型的编程接口。主机代码通常通过普通的C/C++代码进行编写编译之后在CPU上执行而设备代码使用一个特定的C语言方言OpenCL C进行编写这个方言针对并行编程进行了扩展并提供了一系列封装好的数学计算函数。
设备代码上的编译方法有两种在线编译和离线编译。其中在线编译就是指在程序运行时由对应设备厂商开发的OpenCL驱动将设备代码编译为在对应设备上运行的可执行代码离线编译则有两种表现形式第一种是在线编译的扩展版由驱动编译得到的可执行程序可以通过API获取并保存下来当下一需要在同一设备上调用时可以直接使用而不是再次编译第二种则是完全独立的编译过程在OpenCL程序运行之前使用单独的编译工具编译得到可执行文件。
![image-20241020155656219](./heterogeneous-programming-model/image-20241020155656219.webp)
在提出离线编译之后为了让驱动编译好的二进制文件可以在不同的设备之间复用同时也是支持更为丰富的编译器生态系统OpenCL的提出者Khronos设计了一种跨设备的、可迁移的中间表示形式[SPIRV](https://www.khronos.org/spir/)。这种中间形式的提出使得编程语言的提出者、编译器的开发人员可以直接将语言编译为`SPIRV`内核,这样就可以在任何支持`SPIRV`的OpenCL驱动上运行。下面将会介绍的`SYCL`和`Julia`语言都是基于`SPIRV`的中间语言进行构建的。`SPIRV`中间语言的提出也扩展了可以支持`OpenCL`的设备范围,现在已经有开发者和公司在探索将`SPIRV`编译到`Vulkan`、`DirectX`和`Metal`等传统意义上的图形API。
下面是一个使用OpenCL进行矩阵计算的例子。
```cpp
struct ComputationContext
{
cl_platform_id platform;
cl_device_id device;
};
static std::unique_ptr<ComputationContext> selectDevice()
{
cl_uint platformCount;
checkOpenCLError(clGetPlatformIDs(0, nullptr, &platformCount));
std::cout << "Platform count: " << platformCount << std::endl;
std::vector<cl_platform_id> platforms(platformCount);
checkOpenCLError(clGetPlatformIDs(platformCount, platforms.data(), nullptr));
std::unique_ptr<ComputationContext> selectedDevice = nullptr;
for (const auto& platform : platforms)
{
cl_uint deviceCount = 0;
checkOpenCLError(clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, 0, nullptr, &deviceCount));
std::vector<cl_device_id> devices(deviceCount);
checkOpenCLError(clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, deviceCount, devices.data(), nullptr));
for (const auto& device : devices)
{
size_t deviceNameLength;
checkOpenCLError(clGetDeviceInfo(device, CL_DEVICE_NAME, 0, nullptr, &deviceNameLength));
std::vector<char> deviceNameArray(deviceNameLength);
checkOpenCLError(
clGetDeviceInfo(device, CL_DEVICE_NAME, deviceNameLength, deviceNameArray.data(), nullptr));
std::string deviceName(deviceNameArray.data(), deviceNameArray.size() - 1);
std::cout << "Found device: " << deviceName << std::endl;
if (deviceName.find("4060") != std::string::npos)
{
std::cout << "Select device '" << deviceName << "' as runner." << std::endl;
selectedDevice = std::make_unique<ComputationContext>();
selectedDevice->platform = platform;
selectedDevice->device = device;
}
else
{
clReleaseDevice(device);
}
}
}
if (selectedDevice == nullptr)
{
std::cout << "Failed to find the target device." << std::endl;
std::exit(EXIT_FAILURE);
}
return selectedDevice;
}
std::vector<int> clCalculateMatrix(const std::vector<int>& a,
const std::vector<int>& b)
{
cl_int error;
const std::unique_ptr<ComputationContext> computationContext = selectDevice();
// A key-value list ends with 0
// See also https://www.khronos.org/registry/OpenCL/specs/3.0-unified/html/OpenCL_API.html#context-properties-table
std::array<cl_context_properties, 3> properties = {
CL_CONTEXT_PLATFORM,
reinterpret_cast<cl_context_properties>(computationContext->platform),
0
};
cl_context context = clCreateContext(properties.data(), 1, &computationContext->device, nullptr, nullptr,
&error);
checkOpenCLError(error);
cl_command_queue queue = clCreateCommandQueueWithProperties(context, computationContext->device, nullptr,
&error);
checkOpenCLError(error);
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
constexpr size_t matrixSize = MATRIX_SIZE * MATRIX_SIZE * sizeof(int);
cl_mem deviceA = clCreateBuffer(context, CL_MEM_READ_ONLY, matrixSize, nullptr, &error);
checkOpenCLError(error);
cl_mem deviceB = clCreateBuffer(context, CL_MEM_READ_ONLY, matrixSize, nullptr, &error);
checkOpenCLError(error);
cl_mem deviceC = clCreateBuffer(context, CL_MEM_READ_WRITE, matrixSize, nullptr, &error);
checkOpenCLError(error);
checkOpenCLError(
clEnqueueWriteBuffer(queue, deviceA, CL_TRUE, 0, matrixSize, a.data(), 0, nullptr,
nullptr));
checkOpenCLError(
clEnqueueWriteBuffer(queue, deviceB, CL_TRUE, 0, matrixSize, b.data(), 0, nullptr,
nullptr));
// Copy result to erase the previous result
checkOpenCLError(
clEnqueueWriteBuffer(queue, deviceC, CL_TRUE, 0, matrixSize, result.data(), 0,
nullptr, nullptr
));
auto source = R"(
#define MATRIX_SIZE 2048
__kernel void calculate(const __global int* a, const __global int* b, __global int* c)
{
const int x = get_global_id(0);
const int y = get_global_id(1);
int result = 0;
for (int i = 0; i < MATRIX_SIZE; i++)
{
result += a[x * MATRIX_SIZE + i] * b[i * MATRIX_SIZE + y];
}
c[x * MATRIX_SIZE + y] = result;
})";
cl_program program = clCreateProgramWithSource(context, 1, &source, nullptr, &error);
checkOpenCLError(error);
checkOpenCLError(clBuildProgram(program, 0, nullptr, "", nullptr, nullptr));
size_t messageSize;
checkOpenCLError(
clGetProgramBuildInfo(program, computationContext->device, CL_PROGRAM_BUILD_LOG, 0, nullptr, &messageSize));
std::vector<char> messageArray(messageSize);
checkOpenCLError(
clGetProgramBuildInfo(program, computationContext->device, CL_PROGRAM_BUILD_LOG, messageSize, messageArray.data(
), nullptr));
std::string message(messageArray.data(), messageSize - 1);
std::cout << "Build log: " << message << std::endl;
cl_kernel kernel = clCreateKernel(program, "calculate", &error);
checkOpenCLError(error);
checkOpenCLError(clSetKernelArg(kernel, 0, sizeof(cl_mem), &deviceA));
checkOpenCLError(clSetKernelArg(kernel, 1, sizeof(cl_mem), &deviceB));
checkOpenCLError(clSetKernelArg(kernel, 2, sizeof(cl_mem), &deviceC));
cl_event event;
constexpr std::size_t globalSize[2] = {MATRIX_SIZE, MATRIX_SIZE};
checkOpenCLError(clEnqueueNDRangeKernel(queue, kernel, 2, nullptr,
globalSize, nullptr, 0, nullptr, &event));
checkOpenCLError(clWaitForEvents(1, &event));
checkOpenCLError(
clEnqueueReadBuffer(queue, deviceC, CL_TRUE, 0, matrixSize, result.data(), 0,
nullptr, nullptr));
clReleaseMemObject(deviceA);
clReleaseMemObject(deviceB);
clReleaseMemObject(deviceC);
clReleaseKernel(kernel);
clReleaseProgram(program);
clReleaseCommandQueue(queue);
clReleaseContext(context);
clReleaseDevice(computationContext->device);
return result;
}
```
从上面的代码中可以看出两点:
- OpenCL的编程比CUDA的更为繁琐因为OpenCL支持的设备种类更多在主机代码上还需要多出一块选择运行设备的代码
- OpenCL在主机代码和核函数的解耦更为彻底核函数直接以字符串的形式存在于主机代码中而各个厂商提供的驱动才是真正的编译器。
测试的运行结果如下:
| 类型 | 运行时间 | 比率 |
| ----------------------------- | -------- | ---- |
| NVIDIA 4060 Ti OpenCL | 173ms | 0.01 |
| Intel UHD Graphics 770 OpenCL | 1020ms | 0.04 |
| CPU | 21255ms | 1.00 |
### SYCL
SYCL是一个使用标准C++编写在各种异构计算设备上运行核函数的抽象层并提供了一套新的API来查找各种设备并管理这些设备上的内存资源和代码执行。这个标准是开发、无版税、跨平台的抽象标准。同时也是因为这是一个**标准**因此需要寻找支持这个标准的编译器才能使用这个标准。按照官网上的说明我们选择了两个看上去还在活跃开发的项目Intel的[oneAPI](https://www.intel.com/content/www/us/en/developer/tools/oneapi/overview.html)和开源的[AdaptiveCpp](https://github.com/AdaptiveCpp/AdaptiveCpp)进行调研考虑到在后文中还将继续介绍oneAPI相关的工作因此这里将重点放在AdaptiveCpp上。
AdaptiveCpp由四个部分组成分别在不同的C++命名空间中提供。
- SYCL Interface实现了SYCL标准中规定的各种类和函数是实际上同用户交互的接口。这些接口实际上可以仍然可以分成主机API和核函数库两个部分。主机API是普通的C++代码负责任务调度、任务管理和平台射别管理等。核函数库包括了这种在编写核函数时可以使用的类和函数这些接口暴露一些后端特定的功能其中的一些甚至需要使用后端特定的方言来编写例如CUDA。
- AdaptiveCpp Runtime运行时实际上实现了设备调度、任务图管理和执行、数据管理、后端管理、任务调度和同步等等功能运行时负责同各种支持后端的运行时交互来实现上述的功能。
![image-20241029123308139](./heterogeneous-programming-model/image-20241029123308139.webp)
- Compiler考虑到在用户编写的代码中可能使用一些特定后端的方言因此普通的C++编译器无法正常编译所有的用户代码。因此用户代码的编译是通过一个名为`acpp`的Python脚本驱动的这个脚本将各个后端的不同编译器暴露为一个统一的编程接口。
- Glue将上述的各个部分连接在一起的胶水代码。一种典型的胶水代码是内核函数的启动代码`kernel launcher`由于启动器中往往涉及到一些后端特定的方言例如CUDA中的`<<<>>>`或者OpenMP中的各种`pragma`因此这些代码通常需要使用特定的编译器进行编译所以这些胶水代码直接以头文件的方式提供以方便在编译时被特定的编译器处理。这些胶水代码将会把核函数包裹为一个合法的C++函数对象,这样运行时就可以获得这个函数对象并控制代码在设备上的运行。
AdaptiveCpp同时支持多种不同的编译流程。
1. 一种通用的一遍编译流程,将核函数编译到一种统一的中间表示形式,这种中间表示形式将在运行时被编译到特定的后端架构上。这种编译流程提供了高度的可移植性和较快的编译速度。这种编译设施支持的后端有:通过`PTX`在NVIDIA的GPU上运行通过`amdgcn`在AMD的GPU上运行通过`SPIR-V`在Intel的GPU上运行通过`SPIR-V`在任何支持OpenCL驱动的设备上运行也可以通过LLVM直接在CPU上运行。
2. 一种为互操作性优化的多遍编译流程在这个流程中AdaptiveCpp将聚合现有的各种LLVM/Clang的编译工具链使得用户可以在单个代码文件中混合编写SYCL和各种特定的编程模型例如CUDA和HIP。使用这个编译流程的好处有亮点1在这种编译流程中可以直接在SYCL代码使用各个特定编译模型中提供最新设备内部优化Intrinsics不用等待SYCL标准的支持2在这种编译流程中可以使用各个厂商提供的优化模板库例如`rocPRIM`和`CUB`。这种编译流程是提供聚合`CUDA`的clang前端和`ROCm`的clang前端来实现的。
3. 一种只将AdaptiveCpp作为函数使用的编程流程。在这种情况AdaptiveCpp作为一个三方库被引入其他的编译器编译流程中。
第一种通用的编译流程显然是泛用性最广的一种编译流程同时也是AdaptiveCpp推荐的编译流程。
![image-20241029163654675](./heterogeneous-programming-model/image-20241029163654675.webp)
下面是一段使用SYCL进行矩阵乘法加速的代码
```cpp
struct CustomDeviceSelector
{
explicit CustomDeviceSelector(std::string vendorName) : _vendorName(std::move(vendorName))
{
}
int operator()(const sycl::device& d) const
{
int deviceRating = 0;
if (d.is_gpu() && d.get_info<sycl::info::device::name>().find(_vendorName) != std::string::npos)
{
deviceRating = 3;
}
else if (d.is_cpu())
{
deviceRating = 1;
}
return deviceRating;
}
private:
std::string _vendorName;
};
static std::vector<int> syclCalculateMatrix(const std::vector<int>& a, const std::vector<int>& b,
const std::string& hint)
{
const CustomDeviceSelector selector(hint);
sycl::queue queue(selector);
const std::string deviceName = queue.get_device().get_info<sycl::info::device::name>();
std::cout << "Select device: " << deviceName << std::endl;
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
sycl::buffer aBuffer(a);
sycl::buffer bBuffer(b);
sycl::buffer resultBuffer(result);
queue.submit([&](sycl::handler& h)
{
const sycl::accessor aBufferAccessor(aBuffer, h, sycl::read_only);
const sycl::accessor bBufferAccessor(bBuffer, h, sycl::read_only);
const sycl::accessor resultBufferAccessor(resultBuffer, h, sycl::write_only);
h.parallel_for(sycl::nd_range<2>({MATRIX_SIZE, MATRIX_SIZE}, {16, 16}), [=](const sycl::nd_item<2>& item)
{
const size_t x = item.get_global_id(0);
const size_t y = item.get_global_id(1);
int temp = 0;
for (size_t k = 0; k < MATRIX_SIZE; ++k)
{
temp += aBufferAccessor[x * MATRIX_SIZE + k] * bBufferAccessor[k * MATRIX_SIZE + y];
}
resultBufferAccessor[x * MATRIX_SIZE + y] = temp;
});
});
sycl::host_accessor resultHostAccessor(resultBuffer, sycl::read_only);
for (size_t i = 0; i < MATRIX_SIZE; ++i)
{
for (size_t j = 0; j < MATRIX_SIZE; ++j)
{
result[i * MATRIX_SIZE + j] = resultHostAccessor[i * MATRIX_SIZE + j];
}
}
return result;
}
```
测试之后的运行结果如下所示:
| 类型 | 运行时间 | 比率 |
| --------------------------- | -------- | ----- |
| Intel UHD Graphics 770 SYCL | 488ms | 0.023 |
| NVIDIA 4060 Ti SYCL | 180ms | 0.008 |
| OpenMP SYCL | 1591ms | 0.076 |
| CPU | 20930ms | 1.000 |
### OpenACC
OpenACC是一个通过编译器制导来在代码中表达并行性并利用并行编译器为多个并行加速器生成代码的编程模型。为了保证OpenACC可以适配于各种计算架构的加速设备OpenACC设计了一个各种并行层次和有着不同速度和寻址方式内存的编程模型。同时OpenACC主要的功能即是支持同时将计算和数据卸载到一个加速设备上考虑到加速设备可能有着同宿主设备完全不同的内存架构OpenACC编译器和运行时将会自动分析代码并负责加速器上内存的管理和加速器和主机之间的数据传输。
作为一个高等级、平台独立的加速器编程框架使用OpenACC进行开发能够使开发人员将一个源代码编译到一系列设备上运行并实现一个相对较好的性能但是这个简易性和移植性也在一定程度上造成使用OpenACC编程无法完全利用加速设备上的算力。
OpenACC是作为一个标准的形式提供的实现了该标准的编译器有
| 编译器名称 | 情况 |
| ------------------------------------------------------------ | ------------------------------------------------------------ |
| NVIDIA HPC SDK | 支持在NVIDIA GPU和多核CPU上的OpenACC并行编程 |
| Sourcery CodeBench Lite | OpenACC官网上说支持针对AMD GPU的编译但是官网页面似乎改版了没有找到相关的内容 |
| GCC 12 | 支持到OpenACC 2.6 |
| [Omni Compiler Project](https://github.com/omni-compiler/omni-compiler) | 源到源编译器,将带有制导的源代码翻译到带有运行时调用的平台代码,近两年没有活跃开发 |
| [OpenUH](https://github.com/uhhpctools/openuh) | 项目开发者在7年前的最后一次提交了中删除了README中有关OpenACC的内容 |
| [OpenArc](https://csmd.ornl.gov/project/openarc-open-accelerator-research-compiler) | 是学术界出品的还在活跃开发的编译器看上去还做了不少工作的样子就是OpenACC官网上的链接已经失效了找起来比较麻烦而且宣称是一个开源编译器但是获取源代码和二进制文件需要联系他们美国橡树岭国家实验室创建账户这看去对于我们这些Foreign Adversary有些抽象了。 |
在试验OpenACC时遇到了巨大的困难不论是使用gcc还是NVIDIA HPC SDK都没有办法实现明显的并行编程加速多次实验之后都没有找到的问题的所在。这里还是贴一下实验的代码和实验的数据。
实验中编写的OpenACC加速代码如下
```cpp
static std::vector<int> OpenACCCpuCalculateMatrix(const std::vector<int>& a, const std::vector<int>& b)
{
constexpr int length = MATRIX_SIZE * MATRIX_SIZE;
const auto aBuffer = new int[length];
const auto bBuffer = new int[length];
const auto cBuffer = new int[length];
for (int i = 0; i < length; i++)
{
aBuffer[i] = a[i];
bBuffer[i] = b[i];
cBuffer[i] = 0;
}
#pragma acc enter data copyin(aBuffer[0:length], bBuffer[0:length])
#pragma acc enter data create(bBuffer[0:length])
#pragma acc data present(aBuffer[0:length], bBuffer[0:length], cBuffer[0:length])
{
#pragma acc kernels loop independent
for (int i = 0; i < MATRIX_SIZE; i++)
{
#pragma acc loop independent
for (int j = 0; j < MATRIX_SIZE; j++)
{
int temp = 0;
#pragma acc loop independent reduction(+:temp)
for (int k = 0; k < MATRIX_SIZE; k++)
{
temp += aBuffer[i * MATRIX_SIZE + k] * bBuffer[k * MATRIX_SIZE + j];
}
cBuffer[i * MATRIX_SIZE + j] = temp;
}
}
}
#pragma acc exit data copyout(cBuffer[0:length])
#pragma acc exit data delete(aBuffer[0:length], bBuffer[0:length])
std::vector result(MATRIX_SIZE * MATRIX_SIZE, 0);
for (int i = 0; i < length; ++i)
{
result[i] = cBuffer[i];
}
delete[] aBuffer;
delete[] bBuffer;
delete[] cBuffer;
return result;
}
```
实验中使用分别使用`NVIDIA HPC SDK`和`GCC`编译运行的结果如下:
| 编译器 | 类型 | 运行时间 |
| -------------- | ------- | -------- |
| NVIDIA HPC SDK | OpenACC | 19315ms |
| NVIDIA HPC SDK | CPU | 22942ms |
| GCC | OpenACC | 19999ms |
| GCC | CPU | 22623ms |
### oneAPI
oneAPI是Intel公司提出的一套异构并行编程框架该框架致力于达成如下几个目标1定义一个跨架构、跨制造商的统一开放软件平台2允许同一套代码可以在不同硬件制造商和加速技术的硬件上运行3提供一套覆盖多个编程领域的库API。为了实现这些目标oneAPI同上文中已经提到过的开放编程标准SYCL紧密合作oneAPI也提供了一个SYCL的编译器和运行时同时oneAPI也提供了一系列API库包括`oneDPL`、`oneDNN`、`oneTBB`和`oneMKL`等。
![image-20241103162259981](./heterogeneous-programming-model/image-20241103162259981.webp)
我对于oneAPI的理解就是Intel用来对标NVIDIA的CUDA的一套高性能编程工具箱。首先为了和NVIDIA完全闭源的CUDA形成鲜明的对比Intel选择了OpenCL合作同时开发SYCL当时也有可能是Intel知道自己的显卡技不如人如果不兼容市面上其他的部件是没有出路的同时为了和CUDA丰富的生态竞争Intel再开发并开源了一系列的`oneXXX`。
这里我就把上面SYCL写的例子用Intel提供的`DPC++`编译运行一下,看看在效率上会不会有所变化。
| 类型 | 运行时间 | 比率 |
| ----------------------------- | -------- | ----- |
| Intel UHD Graphics 770 oneAPI | 429ms | 0.023 |
| NVIDIA 4060 Ti oneAPI | 191ms | 0.010 |
| Intel i5-13600K oneAPI | 198ms | 0.011 |
| CPU | 18643ms | 1.000 |
在显卡上的计算时间没有明显的变化但是我们Intel的编译器却在选择到使用Intel CPU进行计算时展现了不俗的实力。
## 参考文献
1. 刘颖,吕方,王蕾,陈莉,崔慧敏,冯晓兵.异构并行编程模型研究与进展.软件学报,2014,25(7):1459-1475. [http://www.jos.org.cn/1000-9825/4608.htm](http://www.jos.org.cn/1000-9825/4608.htm)
2. AdaptiveCpp官方文档. [https://adaptivecpp.github.io/AdaptiveCpp/](https://adaptivecpp.github.io/AdaptiveCpp/)
3. Exploring the performance of SGEMM in OpenCL on NVIDIA GPUs. [https://github.com/CNugteren/myGEMM](https://github.com/CNugteren/myGEMM)
4. OpenACC Programming and Best Practices Guide. [https://openacc-best-practices-guide.readthedocs.io/en/latest/01-Introduction.html](https://openacc-best-practices-guide.readthedocs.io/en/latest/01-Introduction.html)
5. oneAPI What is it?. [https://www.intel.com/content/www/us/en/developer/articles/technical/oneapi-what-is-it.html](https://www.intel.com/content/www/us/en/developer/articles/technical/oneapi-what-is-it.html)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,101 @@
---
title: High Performance Computing 25 SP CPU Architecture
date: 2025-03-13T23:59:08.8167680+08:00
tags:
- 学习资料
- 高性能计算
---
How to use the newly available transistors?
<!--more-->
Parallelsim:
Instruction Level Parallelism(ILP):
- **Implicit/transparent** to users/programmers.
- Instruction pipelining.
- Superscalar execution.
- Out of order execution.
- Register renaming.
- Speculative execution.
- Branch prediction.
Task Level Parallelism(TLP):
- **Explicit** to users/programmers.
- Multiple threads or processes executed simultaneously.
- Multi-core processors.
Data Parallelism:
- Vector processors and SIMD.
Von Neumann Architecture: the **stored-program** concept. Three components: processor, memory and data path.
Bandwidth: the gravity of modern computer system.
## Instruction Pipelining
Divide incoming instructions into a series of sequential steps performed by different processor unit to keep every part of the processor busy.
Superscalar execution can execute more than one instruction during a clock cycle.
Order of order execution.
Very long instruction word(VLIW): allows programs to explicitly specify instructions to execute at the same time.
EPIC: Explicit parallel instruction computing.
Move the complexity of instruction scheduling from the CPU hardware to the software compiler:
- Check dependencies between instructions.
- Assign instructions to the functional units.
- Determine when instructions are initiated placed together into a single word.
![image-20250313184421305](./hpc-2025-cpu-architecture/image-20250313184421305.webp)
Comparisons between different architecture:
![image-20250313184732892](./hpc-2025-cpu-architecture/image-20250313184732892.webp)
## Multi-Core Processor Gala
Symmetric multiprocessing(SMP): a multiprocessor computer hardware and software architecture.
Two or more identical processors are connected to a **single shared main memory** and have full access to all input and output devices.
> Current trend: computer clusters, SMP computers connected with network.
Multithreading: exploiting thread-level parallelism.
Multithreading allows multiple threads to share the functional units of a single processor in an overlapping fashion **duplicating only private state**. A thread switch should be much more efficient than a process switch.
Hardware approaches to multithreading:
**fine-grained multithreading**:
- Switches between threads on each clock.
- Hide the throughput losses that arise from the both short and long stalls.
- Disadvantages: slow down the execution of an individual thread.
**Coarse-grained multithreading**:
- Switch threads only on costly stalls.
- Limited in its ability to overcome throughput losses
**Simultaneous multithreading(SMT)**:
- A variation on fine-grained multithreading
![image-20250313190913475](./hpc-2025-cpu-architecture/image-20250313190913475.webp)
## Data Parallelism: Vector Processors
Provides high-level operations that work on vectors.
Length of the array also varies depending on hardware.
SIMD and its generalization in vector parallelism approach improved efficiency by the same operation be performed on multiple data elements.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,370 @@
---
title: High Performance Computing 25 SP NVIDIA
date: 2025-08-31T13:50:42.8639950+08:00
tags:
- 高性能计算
- 学习资料
---
Fxxk you, NVIDIA!
<!--more-->
CPU/GPU Parallelism:
Moore's Law gives you more and more transistors:
- CPU strategy: make the workload (one compute thread) run as fast as possible.
- GPU strategy: make the workload (as many threads as possible) run as fast as possible.
GPU Architecture:
- Massively Parallel
- Power Efficient
- Memory Bandwidth
- Commercially Viable Parallelism
- Not dependent on large caches for performance
![image-20250424192311202](./hpc-2025-cuda/image-20250424192311202.webp)
## Nvidia GPU Generations
- 2006: G80-based GeForce 8800
- 2008: GT200-based GeForce GTX 280
- 2010: Fermi
- 2012: Kepler
- 2014: Maxwell
- 2016: Pascal
- 2017: Volta
- 2021: Ampere
- 2022: Hopper
- 2024: Blackwell
#### 2006: G80 Terminology
SP: Streaming Processor, scalar ALU for a single CUDA thread
SPA: Stream Processor Array
SM: Streaming Multiprocessor, containing of 8 SP
TPC: Texture Processor Cluster: 2 SM + TEX
![image-20250424192825010](./hpc-2025-cuda/image-20250424192825010.webp)
Design goal: performance per millimeter
For GPUs, performance is throughput, so hide latency with computation not cache.
So this is single instruction multiple thread (SIMT).
**Thread Life Cycle**:
Grid is launched on the SPA and thread blocks are serially distributed to all the SM.
![image-20250424193125125](./hpc-2025-cuda/image-20250424193125125.webp)
**SIMT Thread Execution**:
Groups of 32 threads formed into warps. Threads in the same wraps always executing same instructions. And some threads may become inactive when code path diverges so the hardware **automatically Handles divergence**.
Warps are the primitive unit of scheduling.
> SIMT execution is an implementation choice. As sharing control logic leaves more space for ALUs.
**SM Warp Scheduling**:
SM hardware implements zero-overhead warp scheduling:
- Warps whose next instruction has its operands ready for consumption are eligible for execution.
- Eligible warps are selected for execution on a prioritized scheduling policy.
> If 4 clock cycles needed to dispatch the same instructions for all threads in a warp, and one global memory access is needed for every 4 instructions and memory latency is 200 cycles. So there should be 200 / (4 * 4) =12.5 (13) warps to fully tolerate the memory latency
The SM warp scheduling use scoreboard and similar things.
**Granularity Consideration**:
Consider that int the G80 GPU, one SM can run 768 threads and 8 thread blocks, which is the best tiles to matrix multiplication: 16 * 16 = 256 and in one SM there can be 3 thread block which fully use the threads.
### 2008: GT200 Architecture
![image-20250424195111341](./hpc-2025-cuda/image-20250424195111341.webp)
### 2010: Fermi GF100 GPU
**Fermi SM**:
![image-20250424195221886](./hpc-2025-cuda/image-20250424195221886.webp)
There are 32 cores per SM and 512 cores in total, and introduce 64KB configureable L1/ shared memory.
Decouple internal execution resource and dual issue pipelines to select two warps.
And in Fermi, the debut the Parallel Thread eXecution(PTX) 2.0 ISA.
### 2012 Kepler GK 110
![image-20250424200022880](./hpc-2025-cuda/image-20250424200022880.webp)
### 2014 Maxwell
4 GPCs and 16 SMM.
![image-20250424200330783](./hpc-2025-cuda/image-20250424200330783.webp)
### 2016 Pascal
No thing to pay attention to.
### 2017 Volta
First introduce the tensor core, which is the ASIC to calculate matrix multiplication.
### 2021 Ampere
The GA100 SM:
![image-20250508183446257](./hpc-2025-cuda/image-20250508183446257.webp)
### 2022 Hopper
Introduce the GH200 Grace Hopper Superchip:
![image-20250508183528381](./hpc-2025-cuda/image-20250508183528381.webp)
A system contains a CPU and GPU which is linked by a NVLink technology.
And this system can scale out for machine learning.
![image-20250508183724162](./hpc-2025-cuda/image-20250508183724162.webp)
Memory access across the NVLink:
- GPU to local CPU
- GPU to peer GPU
- GPU to peer CPU
![image-20250508183931464](./hpc-2025-cuda/image-20250508183931464.webp)
These operations can be handled by hardware accelerated memory coherency. Previously, there are separate page table for CPU and GPU but for GPU to access memory in both CPU and GPU, CPU and GPU can use the same page table.
![image-20250508184155087](./hpc-2025-cuda/image-20250508184155087.webp)
### 2025 Blackwell
![image-20250508184455215](./hpc-2025-cuda/image-20250508184455215.webp)
### Compute Capability
The software version to show hardware version features and specifications.
## G80 Memory Hierarchy
### Memory Space
Each thread can
- Read and write per-thread registers.
- Read and write per-thread local memory.
- Read and write pre-block shared memory.
- Read and write pre-grid global memory.
- Read only pre-grid constant memory.
- Read only pre-grid texture memory.
![image-20250508185236920](./hpc-2025-cuda/image-20250508185236920.webp)
Parallel Memory Sharing:
- Local memory is per-thread and mainly for auto variables and register spill.
- Share memory is pre-block which can be used for inter thread communication.
- Global memory is pre-application which can be used for inter grid communication.
### SM Memory Architecture
![image-20250508185812302](./hpc-2025-cuda/image-20250508185812302.webp)
Threads in a block share data and results in memory and shared memory.
Shared memory is dynamically allocated to blocks which is one of the limiting resources.
### SM Register File
Register File(RF): there are 32KB, or 8192 entries, register for each SM in G80 GPU.
The tex pipeline and local/store pipeline can read and write register file.
Registers are dynamically partitioned across all blocks assigned to the SM. Once assigned to a block the register is **not** accessible by threads in other blocks and each thread in the same block only access registers assigned to itself.
For a matrix multiplication example:
- If one thread uses 10 registers and one block has 16x16 threads, each SM can contains three thread blocks as one thread blocks need 16 * 16 * 10 =2,560 registers and 3 * 2560 < 8192.
- But if each thread need 11 registers, one SM can only contains two blocks once as 8192 < 2816 * 3.
More on dynamic partitioning: dynamic partitioning gives more flexibility to compilers and programmers.
1. A smaller number of threads that require many registers each.
2. A large number of threads that require few registers each.
So there is a tradeoff between instruction level parallelism and thread level parallelism.
### Parallel Memory Architecture
In a parallel machine, many threads access memory. So memory is divided into banks to achieve high bandwidth.
Each bank can service one address per cycle. If multiple simultaneous accesses to a bank result in a bank conflict.
Shared memory bank conflicts:
- The fast cases:
- All threads of a half-warp access different banks, there's no back conflict.
- All threads of a half-warp access the identical address ,there is no bank conflict (by broadcasting).
- The slow cases:
- Multiple threads in the same half-warp access the same bank
## Memory in Later Generations
### Fermi Architecture
**Unified Addressing Model** allows local, shared and global memory access using the same address space.
![image-20250508193756274](./hpc-2025-cuda/image-20250508193756274.webp)
**Configurable Caches** allows programmers to configure the size if L1 cache and the shared memory.
The L1 cache works as a counterpart to shared memory:
- Shared memory improves memory access for algorithms with well defined memory access.
- L1 cache improves memory access for irregular algorithms where data addresses are not known before hand.
### Pascal Architecture
**High Bandwidth Memory**: a technology which enables multiple layers of DRAM components to be integrated vertically on the package along with the GPU.
![image-20250508194350572](./hpc-2025-cuda/image-20250508194350572.webp)
**Unified Memory** provides a single and unified virtual address space for accessing all CPU and GPU memory in the system.
And the CUDA system software doesn't need to synchronize all managed memory allocations to the GPU before each kernel launch. This is enabled by **memory page faulting**.
## Advanced GPU Features
### GigaThread
Enable concurrent kernel execution:
![image-20250508195840957](./hpc-2025-cuda/image-20250508195840957.webp)
And provides dual **Streaming Data Transfer** engines to enable streaming data transfer, a.k.a direct memory access.
![image-20250508195938546](./hpc-2025-cuda/image-20250508195938546.webp)
### GPUDirect
![image-20250508200041910](./hpc-2025-cuda/image-20250508200041910.webp)
### GPU Boost
GPU Boost works through real time hardware monitoring as opposed to application based profiles. It attempts to find what is the appropriate GPU frequency and voltage for a given moment in time.
### SMX Architectural Details
Each unit contains four warp schedulers.
Scheduling functions:
- Register scoreboard for long latency operations.
- Inter-warp scheduling decisions.
- Thread block level scheduling.
### Improving Programmability
![image-20250515183524043](./hpc-2025-cuda/image-20250515183524043.webp)
**Dynamic Parallelism**: The ability to launch new grids from the GPU.
And then introduce data-dependent parallelism and dynamic work generation and even batched and nested parallelism.
The cpu controlled work batching:
- CPU program limited by single point of control.
- Can run at most 10s of threads.
- CPU is fully consumed with controlling launches.
![](./hpc-2025-cuda/image-20250515184225475.webp)
Batching via dynamic parallelism:
- Move top-level loops to GPUs.
- Run thousands of independent tasks.
- Release CPU for other work.
![image-20250515184621914](./hpc-2025-cuda/image-20250515184621914.webp)
### Grid Management Unit
![image-20250515184714663](./hpc-2025-cuda/image-20250515184714663.webp)
Fermi Concurrency:
- Up to 16 grids can run at once.
- But CUDA streams multiplex into a single queue.
- Overlap only at stream edge.
Kepler Improved Concurrency:
- Up to 32 grids can run at once.
- One work queue per stream.
- Concurrency at full-stream level.
- No inter-stream dependencies.
It is called as **Hyper-Q**.
Without Hyper-Q:
![image-20250515185019590](./hpc-2025-cuda/image-20250515185019590.webp)
With Hyper-Q:
![image-20250515185034758](./hpc-2025-cuda/image-20250515185034758.webp)
In pascal, **asynchronous concurrent computing** is introduced.
![image-20250515185801775](./hpc-2025-cuda/image-20250515185801775.webp)
### NVLink: High-Speed Node Network
![image-20250515185212184](./hpc-2025-cuda/image-20250515185212184.webp)
> The *consumer* prefix means the product is designed for gamers.
>
> The *big* prefix means the product is designed for HPC.
### Preemption
Pascal can actually preempt at the lowest level, the instruction level.
![image-20250515190244112](./hpc-2025-cuda/image-20250515190244112.webp)
### Tensor Core
Operates on a 4x4 matrix and performs: D = A x B + C.
![image-20250515190507199](./hpc-2025-cuda/image-20250515190507199.webp)
### GPU Multi-Process Scheduling
- Timeslice scheduling: single process throughput optimization.
- Multi process service: multi-process throughput optimization.
How about multi-process time slicing:
![image-20250515190703918](./hpc-2025-cuda/image-20250515190703918.webp)
Volta introduces the multi-process services:
![image-20250515191142384](./hpc-2025-cuda/image-20250515191142384.webp)

BIN
source/posts/hpc-2025-cuda/image-20250424192311202.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/hpc-2025-cuda/image-20250424192825010.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/hpc-2025-cuda/image-20250424193125125.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/hpc-2025-cuda/image-20250424195111341.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/hpc-2025-cuda/image-20250424195221886.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/hpc-2025-cuda/image-20250424200022880.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/hpc-2025-cuda/image-20250424200330783.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/hpc-2025-cuda/image-20250508183446257.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/hpc-2025-cuda/image-20250508183528381.webp (Stored with Git LFS) Normal file

Binary file not shown.

BIN
source/posts/hpc-2025-cuda/image-20250508183724162.webp (Stored with Git LFS) Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More