-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathsearch.json
More file actions
1 lines (1 loc) · 490 KB
/
Copy pathsearch.json
File metadata and controls
1 lines (1 loc) · 490 KB
1
[{"title":"ai-assisted-agricultural-research","url":"/posts/89217a21.html","content":"AI 辅助农学科研建设方案引言在农学科研中,研究者每天面对大量的论文文献、实验数据、田间观测记录。如何高效地管理这些知识资产,并将 AI 能力融入科研全流程——从文献调研、实验设计、图像识别到论文写作——是提升团队科研效率的关键。\n本文基于一个真实的教授团队场景(10+ 人),提供一套完整的 AI 辅助农学科研建设方案。方案采用混合架构:本地部署保障数据安全,云端 API 提供强大推理能力,开源工具覆盖图像识别等专业需求。\n\n一、总体架构设计原则\n数据安全:原始论文和实验数据存储在本地的 NAS 和工作站,不上传云端\n成本可控:一次性硬件投入约 5.5 万,月均运营费用 600-1,100 元\n渐进建设:按五层架构逐步搭建,每层独立可用\n团队共享:所有成员通过浏览器访问,无需安装客户端\n\n五层架构全景第五层: 基因与分子育种工具\n AlphaFold 3 / CRISPR Design / GWAS\n ─────────────────────────────────────\n第四层: 自动化报告与智能推送\n Hermes Agent / Telegram / 定时日报\n ─────────────────────────────────────\n第三层: 气象数据与产量预测\n NASA POWER / OpenMeteo / 作物生长模型\n ─────────────────────────────────────\n第二层: 作物图像识别系统\n YOLOv11 + PlantCV + 病虫害数据集\n ─────────────────────────────────────\n第一层: 智能知识库与 Agent(已建成)\n Dify + RAG + 联网搜索 + 代码执行\n ─────────────────────────────────────\n底层: GPU 工作站 + NAS 存储\n RTX 4090 / 128GB / 8TB RAID1\n\n\n二、硬件基础设施(已建)推荐硬件配置\n\n\n组件\n型号\n数量\n预算\n\n\n\nCPU\n双路 Xeon Silver 4416+ / 32 核\n1\n含整机\n\n\n内存\n128GB DDR5 ECC\n1\n含整机\n\n\nGPU\nRTX 4090 24GB\n1\n含整机\n\n\n系统盘\n2TB NVMe SSD\n1\n含整机\n\n\n数据盘\n8TB SATA RAID1\n2\n含整机\n\n\n工作站\nDell Precision / 联想 P 系列\n1\n~50,000 元\n\n\nNAS\n群晖 DS224+\n1\n~2,200 元\n\n\nNAS 硬盘\n4TB 红盘 RAID1\n2\n~1,200 元\n\n\nUPS\nAPC Back-UPS 1500VA\n1\n~1,000 元\n\n\n合计\n\n\n~55,000 元\n\n\n存储策略NAS(冷存储): 原始 PDF 论文、团队共享文件、工作站备份\n → RAID1 镜像,一块硬盘损坏不丢数据\n \n工作站(热存储): 向量索引、Milvus 数据库、Dify 应用数据\n → 每日凌晨 rsync 增量备份到 NAS\n \n移动硬盘(离线冷备): 每月一次重要数据全量备份\n\n\n三、第一层:智能知识库与 Agent(已完成)软件栈\n\n\n组件\n选型\n说明\n\n\n\nAI 平台\nDify(开源)\nLLM 应用开发平台,带 RAG Pipeline\n\n\n向量数据库\nQdrant\nDify 内置,零配置\n\n\nEmbedding 模型\nbge-m3(Ollama)\n中文效果优秀\n\n\nLLM\nClaude Sonnet 4 / GPT-4o\n云端 API,按量付费\n\n\n文档解析\nDify 内置 (Unstructured)\n支持 PDF / DOCX / PPT\n\n\n部署步骤# 1. 安装 Docker\ncurl -fsSL https://get.docker.com | bash\n\n# 2. 启动 Ollama(本地 Embedding)\ndocker run -d --gpus all -v ollama:/root/.ollama \\\n -p 11434:11434 --name ollama ollama/ollama\ndocker exec ollama ollama pull bge-m3:latest\n\n# 3. 启动 Dify\ngit clone https://github.com/langgenius/dify.git\ncd dify/docker\ncp .env.example .env\ndocker compose up -d\n\n# 4. 浏览器打开 http://<服务器IP> 配置模型\n# Ollama/bge-m3 (Embedding) + Claude API (LLM)\n\n升级为 Agent在 Dify 中将 Chat Bot 改为 Agent,添加工具:\n\n联网搜索:对接 Bing Search API 或 SerpAPI\n代码执行:内置 Python 代码解释器\n天气查询:自定义工具对接 OpenMeteo(免费)\n知识库检索:已内置\n\nAgent 系统提示词:\n你是一个农学 AI 助手。\n- 专业问题优先检索知识库中的论文资料\n- 需要最新数据时(气象、价格、政策),先联网搜索\n- 涉及计算和分析时,使用代码执行工具\n- 回答时引用信息来源\n- 不确定时如实告知,不编造\n\n\n四、第二层:作物图像识别系统这是 AI 在农学中应用最成熟的方向,也是本方案中投入产出比最高的扩展。\n技术选型\n\n\n工具\n用途\n许可证\n\n\n\nYOLOv11\n目标检测(病虫害斑点、果实计数)\n开源\n\n\nPlantCV\n植物表型定量分析(叶片面积、病斑占比)\n开源\n\n\nRoboFlow\n数据集标注与管理\n免费额度\n\n\nLabelStudio\n自托管标注平台\n开源\n\n\nYOLOv11 部署# 在工作站上安装 YOLOv11\npip install ultralytics\n\n# 使用预训练模型进行推理(零训练即可用)\nyolo predict model=yolo11n.pt source=crop_photo.jpg\n\n# 微调自定义数据集\nyolo train model=yolo11n.pt data=plant_disease.yaml epochs=100\n\n在 RTX 4090 上,YOLOv11 推理一张图片仅需 5-15ms,可实时处理。\nPlantCV 部署pip install plantcv\n\n# 叶片面积测量示例\nfrom plantcv import plantcv as pcv\npcv.params.debug = \"plot\"\nimg, path, filename = pcv.readimage(\"leaf.jpg\")\nmask = pcv.threshold.binary(gray_img, 120, 255, \"light\")\narea = pcv.analyze.size(img, mask)\nprint(f\"叶片面积: {area} 像素\")\n\n完整工作流田间拍照(手机/无人机)\n ↓\n上传到 Dify Agent\n ↓\nAgent 调用 YOLOv11 → 检测病斑位置\n ↓\nAgent 调用 PlantCV → 计算病斑面积占比\n ↓\nAgent 检索知识库 → 匹配病害类型和防治方法\n ↓\n输出完整诊断报告(含置信度、防治建议)\n\n公开数据集(无需从头标注)\n\n\n数据集\n内容\n规模\n\n\n\nPlantVillage\n作物病害图片\n54,000+ 张\n\n\nAI Challenger 2018\n农作物病虫害(中文)\n50,000+ 张\n\n\nIP102\n昆虫害虫分类\n75,000+ 张\n\n\n水稻病害数据集\n稻瘟病/白叶枯/纹枯病\n多个开源版本\n\n\n\n五、第三层:气象数据与产量预测数据源\n\n\nAPI\n覆盖范围\n费用\n用途\n\n\n\nNASA POWER\n全球历史气象数据\n免费\n历史气象分析、趋势研究\n\n\nOpenMeteo\n全球天气预报\n免费\n近期农事决策支持\n\n\nCMA 气象数据\n中国区域\n部分免费\n国内气象站数据\n\n\n在 Dify 中封装为工具# 自定义 Dify 工具:查询气象数据\nimport requests\n\ndef get_weather_data(location: str, start_date: str, end_date: str):\n \"\"\"获取指定地点和时间范围的气象数据\"\"\"\n url = \"https://archive-api.open-meteo.com/v1/archive\"\n params = {\n \"latitude\": get_lat(location),\n \"longitude\": get_lon(location),\n \"start_date\": start_date,\n \"end_date\": end_date,\n \"daily\": [\"temperature_2m_max\", \"precipitation_sum\", \"relative_humidity_2m_mean\"],\n \"timezone\": \"Asia/Shanghai\"\n }\n response = requests.get(url, params=params)\n return response.json()\n\n应用场景"今年湖南晚稻生长季(6-9月)降雨量是否异常?"\n → Agent 调用气象 API 获取数据\n → 与历史 10 年均值对比\n → 检索知识库中关于"降雨量对稻瘟病影响"的论文\n → 给出风险评级和防治建议\n\n"预测本季度玉米产量"\n → 获取气象数据 + 历史产量数据\n → 调用 Python 代码执行随机森林预测\n → 输出产量区间和置信度\n\n\n六、第四层:自动化报告与智能推送Hermes Agent 部署Hermes Agent(Nous Research 出品)是一个自托管的开源 AI 智能体,具有持久记忆、自动技能创建、多平台接入和定时任务能力。\n# 一条命令安装\ncurl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash\n\n# 启动\nhermes\n\n定时日报配置每天早上 7:00:\n 1. 联网搜索今日农业新闻\n 2. 查询当地天气预报\n 3. 检索知识库中的当日农事建议\n 4. 整合生成农学日报\n 5. 推送到团队 Telegram 群 / 微信群\n\n适用角色\n\n\n谁用\n用 Dify 还是 Hermes\n\n\n\n全体团队\nDify Web — 查论文、做问答\n\n\n教授/管理员\nDify + Hermes — 团队共享 + 个人自动化\n\n\n研究生\nDify — 写论文时查资料\n\n\n\n七、第五层:基因与分子育种工具(可选)如果团队涉及分子育种方向,以下工具与 Dify 知识库形成完整闭环:\n\n\n\n工具\n用途\n\n\n\nAlphaFold 3\n预测蛋白质三维结构,理解抗病基因功能\n\n\nCRISPR Design Tools\nAI 辅助设计基因编辑靶点\n\n\nAlphaMissense\n预测错义突变对蛋白质功能的影响\n\n\nDeepSEA / Enformer\nDNA 序列功能预测\n\n\nPLINK + AI\n全基因组关联分析(GWAS)\n\n\n典型场景:\n"想培育抗旱水稻品种"\n → 知识库检索已有抗旱基因研究\n → AlphaFold 预测相关蛋白结构\n → AI 分析哪些突变可能增强抗旱性\n → CRISPR 工具设计编辑方案\n → 田间试验 → 图像识别记录表型 → 数据反馈优化\n\n\n八、AI 辅助论文写作现有知识库也可以反哺论文产出:\n\n\n\n环节\nAI 工具\n用途\n\n\n\n文献调研\nDify 知识库 + Connected Papers\n快速定位核心文献\n\n\n数据分析\nClaude / GPT 辅助写 Python/R 代码\n加速统计和可视化\n\n\n论文润色\nDeepL Write / Claude\n学术英语润色\n\n\n图表制作\nMatplotlib + AI 辅助\n出版级图表\n\n\n参考文献\nZotero + AI 插件\n自动格式化引用\n\n\n\n九、实施路线图第 1 周(已建): ✅ Dify 知识库\n 硬件就位 + NAS 存储 + PDF 论文向量化 + 团队 Web 访问\n \n第 2 周: Dify 升级为 Agent\n + 联网搜索工具\n + 代码执行工具\n + 天气/气象工具\n + Agent 提示词优化\n \n第 3-4 周: 作物图像识别\n + 部署 YOLOv11 + PlantCV\n + 标注/收集作物图片数据集\n + 微调模型\n + 接入 Dify 自定义工具\n \n第 5 周: 气象预测接口\n + 封装 NASA POWER + OpenMeteo\n + 实现气象 + 知识库联合分析\n \n第 6 周(可选): 自动化推送\n + 安装 Hermes Agent\n + 配置定时日报\n + 接入即时通讯平台\n \n长期(可选): 分子育种方向\n + 按需接入 AlphaFold / CRISPR\n\n\n十、预算汇总\n\n\n项目\n一次性投入\n月费\n\n\n\nGPU 工作站\n~50,000 元\n—\n\n\nNAS + 硬盘\n~3,400 元\n—\n\n\nUPS 等配件\n~1,200 元\n—\n\n\nClaude / GPT API\n—\n~400-800 元\n\n\n联网搜索 API\n—\n~30 元\n\n\n电费\n—\n~200-300 元\n\n\n合计\n~55,000 元\n~600-1,100 元/月\n\n\n与传统方案对比\n\n\n维度\n传统方式\n本方案\n\n\n\n文献检索\n手动翻论文,耗时\nAI 知识库秒级检索\n\n\n病虫害诊断\n请专家肉眼判断\nAI 图像识别 + 知识库\n\n\n气象分析\n手动查气象站数据\nAPI 自动获取 + AI 分析\n\n\n团队协作\n各自为战,知识分散\n统一知识库,全员共享\n\n\n论文写作\n从零开始\nAI 辅助润色 + 资料整合\n\n\n\n十一、总结本方案的核心价值在于:\n\n渐进式建设:五层架构每层独立可用,从第一天的知识库问答到第六周的自动化推送\n数据安全:原始数据存本地,云端只传检索文本片段\n低运营成本:月费 600-1,100 元,远低于单独购买 AI 服务\n全员可用:十余人团队通过浏览器共享一套系统\n覆盖全流程:从文献调研、田间识别、气象预测到论文写作\n\n这套方案不仅适用于农学,稍作调整即可推广到其他学科团队。核心在于:将 AI 能力与学科专业知识深度结合,让研究者从重复劳动中解放出来,聚焦于创造性科研工作。\n","categories":["ai"],"tags":["AI","农学","科研","知识库","RAG","YOLO","PlantCV","Dify","作物表型","智慧农业"]},{"title":"Claude Mem 深度调研:为 AI 编程助手装上持久化记忆","url":"/posts/ebd7c5c3.html","content":"简介Claude Code 默认是无状态的——每次新会话都从零开始,你需要重新解释项目结构、代码规范和历史决策。这就像每次上班都要重新入职一样低效。\nclaude-mem 由 Alex Newman(@thedotmack)于 2025 年 8 月创建,是一个为 Claude Code 等 AI 编程助手提供跨会话持久化记忆的开源系统。GitHub 斩获 74000+ Star,目前最新版本 v13.3.0(2026 年 5 月)。\n它解决的核心问题是:让 AI 记住每一次编码会话的关键决策、Bug 修复和技术发现,并在未来的会话中自动注入相关上下文。\n核心架构claude-mem 由四大核心组件构成:\n[会话行为流] → [记忆引擎] → [压缩存储] → [上下文注入]\n\n1. Worker 服务(Bun HTTP Server)基于 Bun 运行时的高性能 HTTP 服务,默认监听 127.0.0.1:37777,负责:\n\n接收来自 Hook 脚本的事件数据\n管理 SQLite 数据库读写\n调用 AI 模型进行观察压缩和摘要生成\n提供 Web UI 实时查看记忆流\n提供 REST API 供外部查询\n\n2. 数据库层(SQLite + Chroma)采用混合存储策略:\n\nSQLite(WAL 模式):存储会话、观察记录、摘要的结构化数据,启用 FTS5 全文搜索\nChroma 向量数据库:存储语义向量,支持相似度搜索\n所有数据存储在 ~/.claude-mem/ 目录下,不离开本机\n\n核心数据表:\n\n\n\n表名\n用途\n\n\n\nsdk_sessions\n会话记录,含项目名、平台来源、状态\n\n\nobservations\n每次工具调用的观察记录\n\n\nsession_summaries\n会话结束后的 AI 生成摘要\n\n\n3. MCP Server一个轻量级包装层(mcp-server.cjs),将 MCP 协议翻译为 Worker 的 HTTP API 调用,向 Claude Code 暴露搜索、时间线、详情获取等工具。\n4. Hook 脚本系统通过 Claude Code 的 Hook 机制注册到五个生命周期节点。\n五大生命周期 Hookclaude-mem 在 Claude Code 的每个关键节点注入逻辑,形成完整的「感知-记录-总结-注入」闭环:\nSetup Hook在 Claude Code 启动时触发,执行版本检查和插件自检,确保组件就绪。\nSessionStart Hook匹配事件:startup、clear、compact\n执行两个关键任务:\n\n启动 Worker 服务:通过 bun-runner.js 拉起 Worker 进程\n注入历史上下文:调用 context-generator.cjs,从数据库中检索当前项目的相关历史观察,注入到当前会话的 prompt 中\n\nUserPromptSubmit Hook用户每次提交 prompt 时触发,执行会话初始化,确保当前会话在数据库中被正确追踪。\nPostToolUse Hook最核心的 Hook,匹配所有工具调用(*)。每次 Claude 执行 Read、Edit、Bash、Grep 等工具后:\n\n捕获工具名称、参数和结果\n格式化为结构化观察记录\n发送到 Worker 服务进行存储\n触发 AI 压缩(去噪、提取关键信息)\n\nStop Hook会话结束时触发,调用 Worker 的 summarize 接口,由 AI 模型生成结构化摘要,包含:\n\nrequest:用户的请求\ninvestigated:调研的内容\nlearned:学到的知识\ncompleted:完成的工作\nnext_steps:下一步计划\nfiles_read / files_edited:涉及的文件\n\nPreToolUse Hook(Read 专用)在 Claude 读取文件之前触发,将文件上下文信息发送给 Worker,用于丰富记忆的关联性。\n观察(Observation)生成管线claude-mem v13.1.0 引入了 server-beta 事件管线,将观察生成升级为完整的生产级管道:\nAgent Event → Outbox → BullMQ Worker → Observation Row\n\n关键特性:\n\n幂等入队:request-id 端到端传播,保证不重复\n三 AI 提供商支持:Anthropic、OpenAI、Google,可按团队/项目配置\n生成任务重试/取消:支持失败重试和手动取消\n结构化审计日志:完整的操作追踪\n\nMCP 搜索工具:3 层递进式检索claude-mem 的 MCP 搜索设计是其标志性特性,通过递进式信息披露节省约 10 倍 Token:\n\n\n\n层级\n工具\n返回内容\nToken 成本\n\n\n\n第一层\nsearch\n索引摘要(ID、时间戳、类型、标题)\n~50-100/条\n\n\n第二层\ntimeline\n围绕指定观察的时间线上下文\n可变\n\n\n第三层\nget_observations\n完整观察详情\n~500-1000/条\n\n\n核心原则:永远不要在筛选之前获取完整详情。\nMCP 工具清单基础搜索工具(4 个)\n\n\n工具\n功能\n\n\n\nsearch\n全文搜索 + 语义搜索,支持按项目、类型、日期过滤\n\n\ntimeline\n获取指定观察的前后时序上下文\n\n\nget_observations\n批量获取观察完整详情\n\n\nobservation_search\n基于 GIN 倒排索引的 PostgreSQL 全文搜索(server-beta)\n\n\n知识代理工具(6 个,v12.1.0+)\n\n\n工具\n功能\n\n\n\nbuild_corpus\n从观察历史编译过滤后的语料库\n\n\nlist_corpora\n列出所有已构建的语料库\n\n\nprime_corpus\n将语料库加载到 AI 会话中\n\n\nquery_corpus\n对已加载的语料库进行对话式查询\n\n\nrebuild_corpus\n用新数据重建语料库\n\n\nreprime_corpus\n刷新过期的语料库会话\n\n\n观察管理工具(3 个)\n\n\n工具\n功能\n\n\n\nobservation_add\n手动插入观察记录\n\n\nobservation_context\n获取与查询相关的前 N 条观察的上下文注入文本\n\n\nobservation_record_event\n记录 Agent 事件,触发生成任务\n\n\nTree-Sitter AST 工具(3 个)\n\n\n工具\n功能\n\n\n\nsmart_search\n基于 AST 解析的结构化代码搜索,替代 Grep/Glob/Read 发现循环\n\n\nsmart_outline\n获取文件的结构骨架(所有符号及其签名)\n\n\nsmart_unfold\n展开特定符号(函数、类、方法)的完整源码\n\n\n支持 20+ 语言(Python、JavaScript、TypeScript、Go、Rust、C/C++、Java、Ruby、Swift、Kotlin、PHP、Scala、Bash、Haskell、Zig、CSS/SCSS、TOML、YAML、SQL、Markdown、Lua、Elixir)。\nSKILL 体系claude-mem 内置了 15 个 SKILL(可调用的子代理),覆盖开发全流程:\n\n\n\nSKILL\n用途\n\n\n\ndo\n编排子代理执行分阶段实施计划\n\n\nmake-plan\n创建 LLM 友好的分阶段实施计划\n\n\nmem-search\n跨会话搜索历史工作记录\n\n\nsmart-explore\n使用 AST 解析进行低 Token 代码探索\n\n\nlearn-codebase\n系统性地通读整个代码库\n\n\nknowledge-agent\n构建和查询基于记忆的 AI 知识库\n\n\ndesign-is\n基于 Dieter Rams 十大设计原则审核设计\n\n\ntimeline-report\n生成项目完整开发历史的叙事报告\n\n\nweekly-digests\n生成按周划分的串行叙事摘要\n\n\noh-my-issues\n将 Issue 堆积分类为根因集群\n\n\nbabysit\n监控 PR 评审循环直到可合并\n\n\npathfinder\n将代码库映射为功能分组的流程图\n\n\nversion-bump\n语义化版本发布工作流\n\n\nwowerpoint\n将文档转化为分享式幻灯片 PDF\n\n\nhow-it-works\n解释 claude-mem 的工作原理\n\n\n安装与配置一行命令安装npx claude-mem install\n\n或在 Claude Code 内部安装:\n/plugin marketplace add thedotmack/claude-mem\n/plugin install claude-mem\n\n关键配置项配置文件位于 ~/.claude-mem/settings.json:\n\n\n\n配置项\n默认值\n说明\n\n\n\nCLAUDE_MEM_MODEL\nclaude-sonnet-4-5\n用于观察压缩的模型\n\n\nCLAUDE_MEM_WORKER_PORT\n37777\nWorker HTTP 服务端口\n\n\nCLAUDE_MEM_WORKER_HOST\n127.0.0.1\nWorker 绑定地址\n\n\nCLAUDE_MEM_PROVIDER\nclaude\nAI 提供商(claude/openrouter/gemini)\n\n\nCLAUDE_MEM_CONTEXT_OBSERVATIONS\n50\n上下文注入的最大观察数\n\n\nCLAUDE_MEM_LOG_LEVEL\nINFO\n日志级别\n\n\nCLAUDE_MEM_MODE\ncode\n语言模式(code/code–zh/code–ja)\n\n\nCLAUDE_MEM_DATA_DIR\n~/.claude-mem\n数据存储目录\n\n\n支持 13 种 IDEClaude Code、Gemini CLI、OpenCode、Windsurf、OpenClaw、Codex CLI、Copilot CLI、Antigravity、Goose、Crush、Roo Code、Warp、Cursor。\n隐私与多语言隐私保护\n所有数据存储在本地 ~/.claude-mem/,不离开本机\n仅 AI 压缩调用会发送数据到配置的 AI 提供商\n用 <private>...</private> 标签包裹敏感内容,系统会自动排除\nnpx claude-mem uninstall 彻底清除所有数据\n\n多语言模式内置三种语言模式:\n\n\n\n模式\n语言\n\n\n\ncode\n英文\n\n\ncode--zh\n中文\n\n\ncode--ja\n日文\n\n\n通过 CLAUDE_MEM_MODE 配置项切换。\n上下文注入机制注入时机记忆注入从第二个会话开始生效。第一个会话在项目中播种记忆,后续会话自动注入过去相关工作的上下文。\n注入内容每次 SessionStart 时,context-generator 从数据库检索:\n\n最近会话摘要(最近 N 个会话的完成内容)\n相关历史观察(基于项目名和语义相似度)\nToken 节省统计(展示记忆系统的压缩效率)\n\n注入到 CLAUDE.md 和 stdout,Claude 无需调用任何工具即可获得历史上下文。\n快速入门运行 /learn-codebase 可在单次遍历中(约 5 分钟)将整个代码库前端加载到记忆中。\nClaude Code 内置记忆系统Claude Code v2.1.x 起内置了一套未公开的自动记忆系统,与 claude-mem 并存:\n工作原理\n存储格式:纯 Markdown 文件,位于 ~/.claude/projects/<项目编码>/memory/\n索引文件:MEMORY.md,每个会话自动加载到系统提示词中\n四种记忆类型:User(用户偏好)、Feedback(反馈指导)、Project(项目上下文)、Reference(外部资源指针)\n后台提取:一个 extract-memories 子代理在会话结束后自动运行,提取关键洞察\n\n关键限制\n200 行硬上限:MEMORY.md 超过 200 行后,旧条目静默截断\n25KB 字节上限:作为次要限制\n每回合最多 5 个记忆文件\n无向量搜索:通过独立的 Claude Sonnet 侧调用来扫描文件名/描述以选取相关文件\n陈旧度警告:超过 1 天的记忆会被注入陈旧度警告\n\n\n静默遗忘是最危险的——Claude 无法知道记忆被截断,可能导致重复犯错、与之前的架构决策矛盾、反复询问偏好。\n\n与同类方案对比claude-mem vs claude-mem-liteclaude-mem-lite(@sdsrss / @mingfeiqiao)是对原始 claude-mem 的轻量级重设计:\n\n\n\n维度\nclaude-mem(原版)\nclaude-mem-lite\n\n\n\nLLM 调用方式\n每次工具调用触发 Sonnet\n仅在批量刷新时调用(5-10 条一批)\n\n\n每会话 Token 消耗\n~100K-250K\n~1K-4K\n\n\n成本\n基准\n低 50-100 倍,便宜 600 倍+\n\n\n运行时\n长驻 Worker(1.8MB)\n按需启动,立即退出\n\n\n依赖\nBun + Python/uv + Chroma\n仅 Node.js(3 个 npm 包)\n\n\n向量搜索\nChromaDB(Python/MCP)\nSQLite FTS5 + TF-IDF 余弦相似度\n\n\n噪音过滤\nLLM 判断\n确定性代码级过滤\n\n\nclaude-mem-lite 的设计哲学:先用代码过滤,只把重要内容发送给轻量模型。\nclaude-mem vs memsearch(Zilliz/Milvus)\n\n\n维度\nmemsearch\nclaude-mem\n\n\n\n架构\n4 个 Shell Hook + 后台监听进程\nNode.js Worker + Express + React\n\n\n存储\n纯 Markdown(每天一个文件)\nSQLite + Chroma\n\n\n召回方式\n自动注入(Hook 触发语义搜索)\nAgent 驱动(手动调用工具)\n\n\n上下文开销\n零(无 MCP 工具定义)\nMCP 工具定义常驻\n\n\n向量索引\nMilvus(可从 Markdown 重建)\nChroma\n\n\nmemsearch 的亮点在于零 Token 上下文开销——每次 UserPromptSubmit Hook 自动执行语义搜索并将 Top-3 结果注入 prompt,Claude 无需决定是否搜索,它直接获得上下文。\nclaude-mem vs mem0mem0(mem0ai/mem0)是面向生产环境的向量记忆层:\n\n完全基于嵌入向量的语义检索(无 200 行限制)\n无静默截断\n跨会话召回无需显式工具调用\n支持 Claude Code 和 Cowork 双平台\n\n技术栈总览\n\n\n组件\n技术选型\n\n\n\n运行时\nBun(Worker 服务)\n\n\nWeb 框架\nExpress\n\n\n数据库\nSQLite(WAL 模式 + FTS5)\n\n\n向量存储\nChromaDB(Python/uv 依赖)\n\n\n进程管理\n自研 PM2 风格管理器\n\n\nAI 压缩\nAnthropic Claude / OpenAI / Google Gemini\n\n\n前端 UI\nReact(localhost:37777)\n\n\n代码分析\nTree-sitter(20+ 语言支持)\n\n\nMCP 协议\n自研轻量包装层\n\n\n任务队列\nBullMQ(server-beta)\n\n\n总结claude-mem 通过 Hook 驱动的生命周期感知 + AI 压缩 + 混合向量检索,构建了一套完整的 AI 记忆系统。其核心价值在于:\n\n上下文连续性:跨会话记住关键技术决策和 Bug 修复方案,避免重复踩坑\nToken 经济性:3 层递进式检索在提供丰富上下文的同时将 Token 消耗控制到最低\n开发生命周期全覆盖:从代码探索(smart-explore)→ 计划制定(make-plan)→ 执行实施(do)→ PR 审核(babysit)→ Issue 归并(oh-my-issues)→ 版本发布(version-bump),15 个 SKILL 构成完整的 AI 辅助开发工作流\n隐私可控:数据本地化、敏感内容过滤、一键卸载\n\n局限也不容忽视——对 Chroma 的 Python 依赖增加了安装复杂度,每次工具调用的 LLM 压缩会导致较高的 API 成本,而 Claude Code 内置记忆系统的 200 行静默截断在项目规模增大时会成为瓶颈。\n选择合适的方案取决于具体需求:\n\n需要丰富功能和 UI → claude-mem 原版\n追求低开销和简单 → claude-mem-lite\n看重透明性和纯 Markdown → memsearch\n生产级向量记忆 → mem0\n零配置开箱即用 → Claude Code 内置记忆(注意 200 行限制)\n\n","categories":["ai"],"tags":["claude-mem","claude-code","ai-memory","mcp","agent"]},{"title":"nv-embedding-cache","url":"/posts/af2e1963.html","content":"NV Embedding Cache SDK — 项目介绍背景与定位NV Embedding Cache SDK(以下简称 NVE)是 NVIDIA 开源的高性能嵌入向量缓存开发套件。项目聚焦于推荐系统推理场景中的 Embedding 查找加速——这是现代推荐系统中最核心也最昂贵的操作之一。在典型的深度学习推荐模型中,Embedding 表可能包含数亿甚至数十亿的 ID 对应的高维向量,总规模远超单张 GPU 的显存容量。NVE 通过组合 GPU 软件管理缓存、CPU 端哈希表以及远程参数服务器(如 Redis 或 RocksDB),形成多级存储层次结构,使得在显存受限的情况下仍然能够高效地进行 Embedding 查找。\n项目以 Apache 2.0 许可证开源,由 NVIDIA 核心团队维护,版本号遵循年月日格式(当前版本 26.05.0)。代码仓库包含约 15 万行 C++/CUDA 源码、约 2000 行 Python 绑定代码以及完整的测试套件和示例程序。项目依赖了 cuEmbed(NVIDIA 的 CUDA 嵌入优化内核库)和 nvhashmap(NVIDIA 的 GPU 端高性能哈希表)等自研组件,以及 Abseil、RocksDB、Parallel Hashmap、Redis++、pybind11 等第三方库,全部以 git submodule 形式 vendored 在 third_party/ 目录下。\n设计理念:三层次”按需选用”架构NVE 的核心设计哲学是”按需选用”——将功能划分为三个抽象层次,用户可以根据自己的集成深度和性能需求选择合适的使用方式。\n最高层是嵌入层(Embedding Layer)。这一层对外提供完整的 C++ 和 Python 对象,封装了所有嵌入数据的存储和查找服务。用户只需配置好表层次结构(GPU 缓存 → CPU 哈希表 → 远程存储),然后调用 lookup()、insert()、update()、accumulate() 等方法即可完成端到端的嵌入操作。这一层屏蔽了底层数据搬运、缓存淘汰、流同步等复杂性,适合希望快速集成到现有推理框架的用户。目前提供了三种嵌入层实现:GPUEmbeddingLayer(全 GPU 内存)、LinearUVMEmbeddingLayer(GPU 缓存 + 线性系统内存)和 HierarchicalEmbeddingLayer(完整三级缓存层级)。\n中间层是表(Table)。表代表单一的存储位置,每种表封装一种具体的存储后端。GPU 表(GpuTable)使用设备显存,Host 表使用 CPU 端内存(可选用 std::unordered_map、Google Abseil、Parallel Hashmap、NVIDIA nvhashmap 等多种算法实现),Redis 表和 RocksDB 表则分别连接远程 Redis 集群和本地磁盘上的持久化数据库。表层提供了查询和修改的能力,但并不知道其他表的存在——这种多表协调逻辑由上层嵌入层完成。\n最底层是基础组件。这是一组可独立使用的工具和机制,包括 cuEmbed CUDA 内核库(加速 Embedding 查找的 GPU kernel 集合)、EmbeddingCache(GPU 端集合关联缓存实现)、线程池(ThreadPool)、可插拔内存分配器(Allocator)、日志系统等。这些组件可以被上层表直接使用,也可以被高级用户独立集成到自己的软件栈中。\n核心数据流:三级缓存协同NVE 最引人注目的特性是分层缓存架构(Hierarchical Embedding Layer)。当执行一次 Embedding 查找时,数据流按照性能从高到低的顺序依次尝试三个层次。\n第一层是 GPU 端集合关联缓存(Set-Associative Cache)。这是一块在 GPU 显存中管理的快速缓存,使用 CUDA kernel 并行查找标签(Tag),命中时直接将嵌入向量写入输出缓冲区。GPU 缓存的大小由用户配置,通常设为能覆盖最热门的那些 ID 即可。查找过程完全在 GPU 上进行,不需要 CPU 参与,延迟极低。\n第二层是 CPU 端宿主表(Host Table)。如果某些 ID 在 GPU 缓存中未命中,剩余的缺页键会被回传到 CPU 端。CPU 端宿主表使用多线程并行的哈希表进行查找,每条线程独立处理一个分片(Partition),每个分片有独立的读写锁,从而实现高并发读取。CPU 端可选多种哈希表实现:默认的 std::unordered_map、Google 的 Swiss Table 或 Parallel Hashmap。\n第三层是 远程存储(Remote Table)。对于 CPU 端也未命中的键,最终转发到远程参数服务器——可以是 Redis 集群(通过 hiredis + redis++ 库连接),也可以是本地磁盘上的 RocksDB 数据库。这一层通常用于存储全量嵌入向量,容量最大但延迟也最高。\n这种三级缓存架构的关键优势在于:查找过程中,热点 ID 在 GPU 上就能直接命中,冷门 ID 则需要经过两级回退。对于推荐系统典型的二八分布(20% 的热门 ID 覆盖 80% 的访问),GPU 缓存命中率可以轻松达到 90% 以上,大多数查找只需一次 GPU kernel 调用即可完成。\n此外,嵌入层还提供智能插入策略(InsertHeuristic),自动决定哪些键应该被提升到 GPU 缓存中。系统会跟踪每个 ID 的访问频率,并在一段时间内累积缺页键,当累积数量达到可配置的阈值时才批量插入 GPU 缓存,避免频繁的逐条插入带来性能损失。\n灵活的插件系统:多后端存储支持NVE 的插件系统是一套精巧的 C 符号约定 + 动态加载机制。每个插件被编译为独立的共享库(.so),通过 dlopen/dlsym 在运行时按需加载,而不是传统的 C++ 虚基类继承方式。\n插件接口要求每个 .so 导出三个 C 语言链接函数:plugin_ident() 返回插件名称,plugin_developer() 返回开发者信息,enum_host_table_implementations() 枚举注册该插件提供的所有实现。每个实现对应一个 create_{name}_table_factory() 工厂函数,用于按 JSON 配置文件生产具体的表对象。整个加载流程由 load_host_table_plugin() 函数统一管理,先尝试给定路径加载,失败时回退到相对于当前 .so 的路径。\n目前内置了五个插件:NVHM 插件使用 NVIDIA 自研的 nvhashmap GPU 端哈希表,是 GPU 缓存的核心后端;Abseil 插件使用 Google 的高性能 Swiss Table;PHMAP 插件使用 Parallel Hashmap;Redis 插件连接远程 Redis 集群,支持带 TLS 加密的认证连接;RocksDB 插件提供本地磁盘持久化存储,基于 LSM-Tree 引擎。即使不加载任何插件,系统也内置了一个基于 std::unordered_map 的降级实现,保证基础功能可用。\n完善的接口覆盖:C++、C 与 PythonNVE 为用户提供了三种语言的接口,覆盖不同的集成场景。C++ API 是 SDK 的原生接口,通过模板类和虚基类提供类型安全和零开销抽象。所有 GPU 操作均通过执行上下文(ExecutionContext)管理——每个上下文持有独立的 CUDA lookup stream 和 modify stream,允许多线程并发执行查找,同时保证内部缓冲区的资源隔离。C API 提供扁平的函数接口,通过不透明句柄管理表和层的生命周期,使得非 C++ 语言(如 C、Rust、Go 等)也能方便地集成 NVE。Python API 通过 pybind11 绑定暴露,支持与 PyTorch 深度集成,pynve.NVEmbedding 类直接接受 PyTorch 张量作为输入输出,并提供了 torch.nn.Module 风格的包装类方便已有 PyTorch 模型的迁移。\n高性能内核与优化技术为了在 GPU 上达到极致性能,NVE 在多个层面上进行了深度优化。GPU 端的集合关联缓存采用多路组相联架构,替换策略支持随机淘汰、LRU 和 LFU 三种算法,用户可通过 JSON 配置灵活选择。在 CPU 端,所有哈希表都被划分为多个分片,每个分片独立持有读写锁,细粒度锁策略极大地减少了多线程下的锁竞争。NVE 还充分利用了 CPU 的 SIMD 指令集,构建时自动检测宿主机 AVX 支持情况,从 AVX、AVX2 到 AVX-512 选择最优编译路径。模板分发是 NVE 另一项关键优化——以 NVHM 插件为例,通过在 mask_size、key_size、overflow_handler、kernel_size、minimize_psl、auto_shrink、partitioner 等多维度上分发到编译时特化的具体类型,共可生成约 6144 种不同的组合,在热点查找路径上完全避免了运行时虚函数开销。\n总结NVE 是一个面向推荐系统推理场景的高性能 Embedding 加速 SDK,通过 GPU-CPU 混合缓存架构解决了大规模嵌入表在显存受限情况下的高效查找问题。其按需选用的三层抽象架构、灵活的插件化后端、完善的多语言接口以及深度优化的 CUDA 内核和 CPU 并行查找,使得它既可以作为 PyTorch 模型的一个简单替换层快速集成,也可以作为高性能推理框架的底层存储引擎深度定制。对于需要处理数亿到数十亿级别 Embedding 表的大规模推荐系统团队,NVE 提供了一个开箱即用且性能卓越的解决方案。\n","categories":["ai"],"tags":["NVIDIA","Embedding","Cache","GPU","推荐系统"]},{"title":"Planning with Files —— AI Agent 的持久化工作记忆","url":"/posts/8d3dcd9.html","content":"简介2025 年 12 月,Meta 以 20 亿美元收购了 AI Agent 公司 Manus。这家成立仅 8 个月的公司,年收入已突破 1 亿美元。Manus 成功的秘诀是什么?上下文工程(Context Engineering)。\nManus 官方博客中有一段话:\n\n“Markdown 是我在磁盘上的’工作记忆’。由于我以迭代方式处理信息,且活跃上下文有限,Markdown 文件充当了便签、检查点和最终交付物的构建块。”\n\n这正是 planning-with-files 技能的核心思想。\n问题:AI Agent 的记忆困境当前主流的 AI 编程 Agent(Claude Code、Cursor、Copilot 等)普遍面临以下问题:\n挥发性记忆。 Agent 的 TodoWrite 等工具将任务列表保存在上下文窗口中,一旦执行 /clear 或上下文压缩,所有规划信息就消失了。\n目标漂移。 经过 50 次以上的工具调用后,Agent 往往会忘记最初的目标。它开始偏离主干,在无关细节上钻牛角尖。\n错误不可追溯。 错误被即时处理但未被记录。当 Agent 再次遭遇同样的失败时,它会重复相同的错误尝试。\n上下文膨胀。 所有中间结果都被塞进上下文窗口,而不是写入持久化存储。上下文窗口很快被填满,后续推理质量下降。\n方案:三文件模式planning-with-files 定义了一个简单的三文件工作模式:\ntask_plan.md → 追踪阶段与进度\nfindings.md → 存储研究与发现\nprogress.md → 记录会话日志与测试结果\n\n核心原则只有一条:\n上下文窗口 = 内存(易失、有限)\n文件系统 = 磁盘(持久、无限)\n\n→ 重要的东西写进磁盘。\n\ntask_plan.md —— 任务规划这是整个工作的「方向盘」,包含:\n\nGoal:任务目标\nPhases:拆分为 3-7 个阶段,每阶段带 [ ] 复选框和 **Status:** pending/in_progress/complete\nDecisions:重大技术决策及理由\nErrors:记录所有错误及解决方案\n\nfindings.md —— 研究发现这是「知识库」,存放:\n\n搜索结果、文档链接\n技术调研的发现\n决策的理由和权衡\n\nprogress.md —— 进度日志这是「工作日志」,按时间线记录:\n\n每个阶段开始/结束时间\n执行的操作、修改的文件\n测试结果\n错误日志(带时间戳)\n\n五大核心规则\n\n\n规则\n含义\n\n\n\n先建计划\n永远不要在创建 task_plan.md 之前开始工作\n\n\n2 操作规则\n每执行 2 次浏览/搜索操作后,必须更新 findings.md\n\n\n记录所有错误\n即使立即解决的错误也要记录,避免重复踩坑\n\n\n不重复失败\n记录每次尝试,失败后必须换方案\n\n\n持续完成验证\nStop Hook 自动检查所有阶段是否标记为 complete\n\n\nHook 机制:注意力操控planning-with-files 真正的工程创新在于 Hook 系统。它通过 Claude Code 的生命周期钩子,在关键时刻自动介入:\n\n\n\nHook\n触发时机\n行为\n\n\n\nSessionStart\n会话开始时\n自动恢复上次会话状态,注入 catchup 报告\n\n\nPreToolUse\nWrite/Edit/Bash 操作前\n重新读取 task_plan.md,将目标刷新到注意力窗口\n\n\nPostToolUse\nWrite/Edit 操作后\n提醒更新阶段状态\n\n\nStop\nAgent 尝试停止时\n检查所有阶段是否完成,未完成则阻止退出\n\n\nPreCompact\n上下文压缩前\n提醒刷新进度日志,防止信息丢失\n\n\nPreToolUse Hook 是整套机制的关键——它在每次工具调用前将计划重新注入 Agent 的注意力窗口,从根本上解决了目标漂移问题。\n但这也带来了独特的安全风险。2025 年的安全审计揭示了一条危险的攻击链:\nWebSearch(恶意站点) → 内容写入 task_plan.md\n→ Hook 在下一次工具调用前读取 task_plan.md\n→ Hook 在下下次工具调用前再次读取\n→ 恶意指令被无限放大\n\nv2.21.0 版本修复了这一问题:从技能声明中移除 WebFetch / WebSearch,并添加了安全边界规则——网页搜索结果只能写入 findings.md,绝不能写入 task_plan.md。\n并行计划与目录隔离v2.36.0 引入了并行计划支持,允许同一仓库中存在多个独立的计划目录:\n.planning/\n 2026-01-10-backend-refactor/\n task_plan.md / findings.md / progress.md\n 2026-01-10-production-incident/\n task_plan.md / findings.md / progress.md\n\n使用 init-session.sh <slug> 创建独立计划,使用 set-active-plan.sh <plan-id> 切换活跃计划。Hook 会自动解析当前活跃的计划目录。\n此外,对于跨多个会话的长期运营主题,还提供了 Topic Handoff 模式:\nprogress.md ← 运行时间线和简短索引\nhandoffs/<topic>.md ← 当前状态、命令、验证、风险、回滚方案\n\n五问重启测试判断上下文管理是否扎实,只需回答五个问题:\n\n\n\n问题\n答案来源\n\n\n\n我在哪?\ntask_plan.md 中的当前阶段\n\n\n我去哪?\ntask_plan.md 中的剩余阶段\n\n\n目标是什么?\ntask_plan.md 中的 Goal 声明\n\n\n我学到了什么?\nfindings.md\n\n\n我做了什么?\nprogress.md\n\n\n基准测试使用 Anthropic 的 skill-creator 框架进行的正式评估(10 个并行子 Agent,5 类任务,30 项断言,3 次盲测):\n\n\n\n测试配置\n通过率\n通过数\n\n\n\n启用 skill\n96.7%\n29/30\n\n\n未启用 skill\n6.7%\n2/30\n\n\n差值\n+90 个百分点\n+27/30\n\n\n盲测 A/B 对比:启用 skill 的 Agent 在所有 3 次对比中均获胜(评分 10.0 vs 6.0 / 10.0 vs 6.3 / 10.0 vs 8.0)。\n未启用 skill 的 Agent 并非产出低劣——它们生成了可运行的代码、合理的研究对比、详细的迁移计划。但没有任何一个遵循了结构化的规划流程。而这正是 skill 的全部价值所在。\n多平台支持planning-with-files 目前已支持 17+ 个平台和 IDE,包括 Claude Code、Cursor、GitHub Copilot、Gemini CLI、Codex、Kiro、CodeBuddy、FactoryAI、OpenCode、Continue、Pi Agent、OpenClaw、Antigravity、Kilocode、AdaL CLI、Mastra Code、Hermes 等。同时提供阿拉伯语、德语、西班牙语、简体中文和繁体中文版本。\n适用场景适合使用:\n\n需要 3 步以上的多阶段任务\n调研类任务\n构建 / 创建项目\n涉及大量工具调用的任务\n\n不需要使用:\n\n简单问答\n单文件修改\n快速查找\n\n安装与使用npx skills add OthmanAdi/planning-with-files --skill planning-with-files -g\n\n安装后使用 /plan 或 /planning-with-files:plan 启动计划会话。\n总结planning-with-files 本质上是一套编码化的工程纪律。Claude 本身能够做规划,这个 skill 并不赋予它新的能力——而是将一套经过验证的结构化工作流编码为可复用的模式。\n它巧妙地将「上下文窗口是稀缺资源」这一约束转化为了工程优势:通过文件系统建立持久化工作记忆,通过 Hook 系统实现注意力操控,通过阶段状态追踪确保任务闭环。\nManus 用这套方法在 8 个月内从零做到 20 亿美元估值。而 planning-with-files 将同样的模式以开源、跨平台的方式带给了每一位 AI Agent 用户。\n\n项目地址:github.com/OthmanAdi/planning-with-files\n\n","categories":["ai"],"tags":["AI","Agent","Context-Engineering","Claude-Code"]},{"title":"professor-team-knowledge-base","url":"/posts/efd455f3.html","content":"教授团队 AI 知识库建设方案背景教授团队在日常科研和教学中,积累了大量的 PDF 论文、技术文档、教材等资料。随着团队规模的扩大(10+ 人),如何高效地检索和利用这些知识成为一个痛点。本文提出一套完整的 AI 知识库建设方案,采用本地存储 + 云端 LLM 的混合架构,兼顾数据安全与智能问答能力。\n\n一、架构设计核心理念原始论文数据 → 本地存储(安全可控)\n向量检索 → 本地运行(毫秒级响应)\n大模型推理 → 云端 API(按量付费,无需买卡)\n\n整体架构┌──── NAS 存储 ─────────────────────┐\n│ │\n│ 📂 原始 PDF 论文库(冷存储) │\n│ 📂 团队共享上传目录 │\n│ 📂 工作站索引定期备份 │\n└────────────┬───────────────────────┘\n │ SMB/CIFS 挂载\n ▼\n┌──── GPU 工作站 ────────────────────┐\n│ │\n│ 📝 Dify 平台(Web 服务) │\n│ 🔍 Milvus / Qdrant 向量数据库 │\n│ 🧠 bge-m3 Embedding 模型(本地) │\n│ 📚 文档解析 + OCR 预处理 │\n└────────────┬───────────────────────┘\n │ HTTPS(仅传检索文本片段)\n ▼\n┌──── 云端 API ──────────────────────┐\n│ │\n│ Anthropic Claude Sonnet 4 │\n│ / OpenAI GPT-4o │\n│ (高质量问答) │\n│ │\n│ Claude Haiku 3.5 │\n│ / GPT-4o-mini │\n│ (批量处理/简单任务) │\n└────────────────────────────────────┘\n\n数据流1. 团队成员将 PDF 拖入 NAS 共享文件夹\n2. 工作站定时扫描新文件,解析文本 + 向量化\n3. 用户通过浏览器访问 Dify Web 界面提问\n4. 本地向量库检索相关片段\n5. 仅检索文本被发送到云端 API\n6. LLM 生成回答返回给用户\n\n\n二、硬件配置推荐配置(5 万元档)\n\n\n组件\n型号/规格\n数量\n预算\n\n\n\nCPU\n双路 Xeon Silver 4416+ / 32 核\n1\n含在整机\n\n\n内存\n128GB DDR5 ECC\n1\n含在整机\n\n\nGPU\nRTX 4090 24G\n1\n含在整机\n\n\n系统盘\n2TB NVMe SSD\n1\n含在整机\n\n\n数据盘\n8TB SATA (RAID1)\n2\n含在整机\n\n\n整机\n品牌工作站(如 Dell Precision / 联想 P 系列)\n1\n~50,000 元\n\n\nNAS\n群晖 DS224+\n1\n~2,200 元\n\n\nNAS 硬盘\n4TB 红盘 (RAID1)\n2\n~1,200 元\n\n\n交换机\n千兆交换机\n1\n~200 元\n\n\nUPS\nAPC Back-UPS 1500VA\n1\n~1,000 元\n\n\n合计\n\n\n~55,000 元\n\n\n为什么不需要双 GPU?LLM 在云端跑,本地仅做:\n 1. Embedding 向量化 → GPU 加速可选,CPU 也可跑(单次 ~200ms)\n 2. 向量检索 → 纯 CPU 操作,毫秒级\n 3. 文档解析 / OCR → CPU 密集,多核更重要\n\n128GB 内存是关键——Milvus + Embedding 模型 + 文档预处理同时运行不卡顿。\n\n选型说明\n内存的重要性:向量数据库和 Embedding 模型都是内存大户。128GB 可支持数千万条向量检索\nRAID1 数据盘:两块 8TB 硬盘互作镜像,一块坏了数据不丢\nNAS 的定位:只存原始 PDF 论文,不做检索。工作站硬盘坏时,NAS 上的原文不受影响\n\n\n三、软件栈技术选型\n\n\n组件\n选型\n说明\n\n\n\n操作系统\nUbuntu 24.04 LTS\n稳定,生态好\n\n\n容器引擎\nDocker + Docker Compose\n一键部署,易于维护\n\n\nAI 平台\nDify\n开源 LLMOps 平台,带 RAG 工作流\n\n\n向量数据库\nQdrant / Milvus\nDify 内置支持,零配置\n\n\nEmbedding 模型\nbge-m3 (Ollama)\n中文效果优秀,支持多语言\n\n\n文档解析\nDify 内置 (Unstructured)\n支持 PDF / DOCX / PPT\n\n\n可选 OCR\nPaddleOCR\n处理扫描版 PDF\n\n\n反向代理\nNginx\nHTTPS + 域名绑定\n\n\nDify 的核心优势\n可视化知识库管理:上传 PDF 即可,自动分块和向量化\n内置 RAG Pipeline:检索增强生成开箱即用\n多模型支持:可同时配本地 Embedding + 云端 LLM\nWeb 界面:团队成员通过浏览器访问,无需装客户端\nAPI 接口:也可集成到其他系统\n\n\n四、部署步骤Step 1:服务器基础环境# 安装 Ubuntu 24.04 LTS(选择\"最小安装\"即可)\n# 配置静态 IP,确保内网可达\n\n# 安装 Docker\ncurl -fsSL https://get.docker.com | bash\nsudo usermod -aG docker $USER\n\n# 安装 Docker Compose 插件\nsudo apt install docker-compose-plugin\n\n# 安装 Ollama(本地 Embedding 模型)\ndocker run -d --gpus all -v ollama:/root/.ollama \\\n -p 11434:11434 --name ollama ollama/ollama\n\n# 拉取中文 Embedding 模型\ndocker exec ollama ollama pull bge-m3:latest\n\nStep 2:部署 Dify# 克隆 Dify 项目\ngit clone https://github.com/langgenius/dify.git\ncd dify/docker\n\n# 复制环境变量配置\ncp .env.example .env\n\n# 启动所有服务(PostgreSQL + Redis + Qdrant + Dify API + Dify Web)\ndocker compose up -d\n\n# 验证服务\ndocker compose ps\n# 所有服务应为 Up 状态\n\nStep 3:配置 NAS 挂载# NAS 上创建目录结构\n# /volume1/agri_papers/\n# ├── raw/ ← 原始 PDF\n# ├── processed/ ← OCR 后文本\n# └── backup/ ← 工作站备份\n\n# 工作站挂载 NAS\nsudo mkdir -p /mnt/nas\nsudo mount -t cifs //192.168.1.100/agri_papers /mnt/nas \\\n -o username=admin,password=****,iocharset=utf8\n\n# 写入 /etc/fstab 实现开机自动挂载\necho '//192.168.1.100/agri_papers /mnt/nas cifs \\\n credentials=/etc/nas_cred,iocharset=utf8,file_mode=0777,dir_mode=0777 0 0' \\\n | sudo tee -a /etc/fstab\n\nStep 4:配置 Dify 中的模型打开浏览器访问 http://<服务器IP>,进入 Dify 后台:\n添加 Embedding 模型(本地):\n设置 → 模型供应商 → Ollama\n - 模型名称: bge-m3\n - 基础 URL: http://<服务器IP>:11434\n - 点击"保存"\n\n添加 LLM 模型(云端):\n设置 → 模型供应商 → Anthropic\n - API Key: sk-ant-xxxxx\n - 模型: claude-sonnet-4-20250514\n - 点击"保存"\n\n(可选)设置 → 模型供应商 → OpenAI\n - API Key: sk-xxxxx\n - 模型: gpt-4o\n\nStep 5:创建知识库知识库 → 创建知识库\n - 名称: "农学论文库"\n - 上传方式: 从本地上传 PDF\n - Embedding 模型: ollama/bge-m3\n - 分块策略: \n 分块方式: 自动分段\n 最大块大小: 500 tokens\n 块重叠: 50 tokens\n - 检索设置:\n 检索方式: 混合检索(向量 + 关键字)\n 召回条数: 5\n - 确认创建\n\nStep 6:创建问答应用工作室 → 创建应用 → 聊天助手\n - 名称: "农学知识助手"\n - 模型: Claude Sonnet 4\n - 提示词:\n "你是一个农学领域的知识助手。请基于提供的资料回答问题。\n 如果资料中找不到相关信息,请如实告知。\n 回答要专业、准确、简洁。"\n - 关联知识库: 农学论文库\n - 发布 → 获取分享链接\n\n\n五、API 套餐配置价格对比\n\n\n模型\n输入价格\n输出价格\n推荐用途\n\n\n\nClaude Sonnet 4\n$3.00 / M tokens\n$15.00 / M tokens\n核心问答(质量优先)\n\n\nClaude Haiku 3.5\n$0.80 / M tokens\n$4.00 / M tokens\n批量处理、简单总结\n\n\nGPT-4o\n$2.50 / M tokens\n$10.00 / M tokens\n核心问答\n\n\nGPT-4o-mini\n$0.15 / M tokens\n$0.60 / M tokens\n批量处理、降本\n\n\n费用估算以 10 人团队,每人每天 50 轮对话估算:\n每日 token 消耗: 10人 × 50轮 × 2000token = 1,000,000 tokens\n月 token 消耗: ~3,000 万 tokens\n\n方案一(全 Claude Sonnet 4):\n ≈ $90-120 / 月(约 650-850 元)\n\n方案二(全 GPT-4o):\n ≈ $75-100 / 月(约 550-720 元)\n\n推荐策略(混合):\n 日常问答 → Claude Sonnet 4 / GPT-4o\n 批量处理 → Claude Haiku / GPT-4o-mini\n → 月均 400-800 元\n\nAPI 获取方式\n\n\n平台\n充值\n国内访问\n\n\n\nAnthropic\nconsole.anthropic.com,信用卡\n需海外服务器中转或国内代理\n\n\nOpenAI\nplatform.openai.com,信用卡\n同上\n\n\n国内代理\n支付宝/微信支付\n搜索”Claude API 代理”,加价 10-20%\n\n\n\n替代方案:若国内访问不便,可改用 DeepSeek API 或通义千问 API,中文农学场景效果也很好,国内直连无延迟。\n\n\n六、用户使用指南对于团队成员(零学习成本)1. 上传 PDF:\n 电脑打开 → 文件管理器 → 输入 \\\\192.168.1.100\n 找到 agri_papers/raw 文件夹 → 拖入 PDF\n \n2. 提问:\n 浏览器打开 → http://<服务器IP>\n 进入"农学知识助手" → 输入问题 → 等待回答\n \n3. 可以问什么:\n ✅ "水稻稻瘟病的防治方法有哪些?"\n ✅ "2024年关于土壤改良的最新研究"\n ✅ "这篇论文的核心结论是什么?"(引用上传的论文)\n ❌ 不要问与农学无关的问题(模型不会乱答)\n\n对于管理员# 查看 Dify 日志\ncd dify/docker\ndocker compose logs -f api\n\n# 查看 API 使用量\n# Dify Web: 监控 → 日志 → 查看令牌消耗\n\n# 定时同步 NAS 新文件到知识库\n# Dify 知识库支持手动\"重新同步\",或写脚本调用 API\n\n# 备份向量数据库\nrsync -avz /data/dify/volumes /mnt/nas/backup/\n\n\n七、维护与故障处理日常维护\n\n\n频率\n操作\n说明\n\n\n\n每天\n检查 Dify Web 是否可访问\n团队反馈\n\n\n每周\n上传新论文到知识库\n管理员操作\n\n\n每月\n检查 API 用量和费用\n避免超额\n\n\n每季\n系统更新(apt upgrade)\n安全补丁\n\n\n每年\n更换 NAS 硬盘(如有报警)\nRAID 重建\n\n\n常见问题\n\n\n问题\n排查\n解决\n\n\n\nDify 打不开\ndocker compose ps 检查服务\ndocker compose restart\n\n\n回答质量差\n检查检索片段是否正确\n调整分块参数或换 LLM 模型\n\n\nAPI 报错\n检查 API Key 是否过期\n重新配置或充值\n\n\nNAS 无法挂载\nping NAS IP 是否通\n检查网络和 NAS 状态\n\n\n检索慢\n检查内存使用量\n增加向量库索引优化\n\n\n\n八、预算总览\n\n\n项目\n一次性投入\n月费\n\n\n\nGPU 工作站(推荐版)\n~50,000 元\n—\n\n\n群晖 DS224+ NAS\n~2,200 元\n—\n\n\nNAS 硬盘 2×4TB\n~1,200 元\n—\n\n\n千兆交换机\n~200 元\n—\n\n\nUPS 不间断电源\n~1,000 元\n—\n\n\n网线、配件\n~200 元\n—\n\n\nAPI 费用\n—\n~400-800 元\n\n\n电费\n—\n~200-300 元\n\n\n合计\n~55,000 元\n~600-1,100 元/月\n\n\n相比之下,如果团队成员每人去购买 Claude Pro 订阅($20/月/人),10 人每月就是 $200(约 1,400 元)。这套方案不仅更便宜,还拥有私有知识库、团队共享、数据安全等多人协作独有的能力。\n\n九、总结这套方案的核心优势在于:\n\n数据安全:原始论文存于本地 NAS,上传到云端 API 的只有检索后的文本片段\n成本可控:一次性硬件投入约 5.5 万,月费仅 600-1,100 元,远低于购买大算力 GPU\n即装即用:基于 Dify 平台,团队成员只需浏览器即可使用,无需任何配置\n可扩展:从农学出发,未来可以扩展到其他学科领域,只需更换知识库内容\n开箱即用:从 Day 1 下单硬件到 Day 7 团队上线,一周内即可完成\n\n对于教授团队来说,这是一条投入产出比极高的 AI 知识库建设路径。\n","categories":["ai"],"tags":["AI","知识库","RAG","Dify","大模型","团队部署","私有部署"]},{"title":"Google Abseil C++ 基础库详解","url":"/posts/8ee50b98.html","content":"简介Abseil 是 Google 开源的 C++ 基础库,从 Google 内部代码库中提取而来,在 Search、Gmail、YouTube 等大规模生产环境中经过了十多年的实战检验。Abseil 的设计定位是对 C++ 标准库的补充,而非替代——它填补标准库的空白,并在标准库设计不够理想的地方提供 Google 认为更优的替代方案。\n截止到 2025 年,Abseil 已在 Meta、Tencent、ByteDance 等公司被广泛采用,最新版本为 20250814.1,大约每 6 个月发布一个 LTS 版本。要求 C++17 以上编译器,支持 GCC、Clang、MSVC,构建系统支持 Bazel 和 CMake。\n设计理念补充而非替代Abseil 的设计目标是填补 C++ 标准库的空白——它提供标准库缺少的功能(如 StrCat、StrSplit、命令行参数解析),也提供标准库设计的替代方案(如 flat_hash_map 替代 unordered_map、Time 替代 chrono)。\n不保证 ABI 兼容Abseil 保证 API 级别的向后兼容,并提供自动化工具帮助代码迁移。但它明确不保证 ABI 稳定性——使用者必须使用一致的编译器参数从源码编译 Abseil。这意味着如果将 Abseil 用在动态库中,需要格外注意版本一致性。\nLive at HeadAbseil 鼓励用户直接依赖最新源码而非特定发布版本。这是 Google 内部单一仓库模式的延伸——通过确保构建中只有一个版本的库,消除菱形依赖问题。团队提供自动化迁移工具来平滑升级。\n性能优先每一个组件都反映了大规模场景下的优化成果。设计上优先考虑实际性能而非理论优雅——例如 absl::Time 采用具体类型而非像 std::chrono 那样的模板,因为 Google 发现具体类型在多数服务端应用中更实用。\n核心组件字符串:absl/stringsAbseil 提供了丰富的字符串处理工具,在日常开发中使用频率最高。\nabsl::StrCat() / absl::StrAppend() —— 高效的字符串拼接,避免中间临时对象的分配,性能远优于 + 运算符拼接:\nstd::string path = absl::StrCat(\"/api/v1/user/\", user_id, \"/profile\");\nabsl::StrAppend(&path, \"?format=\", format);\n\nabsl::StrSplit() —— 将字符串按分隔符拆分为 vector<string_view>,零拷贝:\nstd::vector<absl::string_view> parts = absl::StrSplit(\"a,b,c\", ',');\n// parts = [\"a\", \"b\", \"c\"]\n\n// 支持跳过空白\nparts = absl::StrSplit(\"a, b, c\", ',', absl::SkipWhitespace());\n\nabsl::StrJoin() —— 将容器元素拼接为字符串:\nstd::vector<std::string> v = {\"foo\", \"bar\", \"baz\"};\nstd::string result = absl::StrJoin(v, \"-\"); // \"foo-bar-baz\"\n\n// 自定义格式化\nresult = absl::StrJoin(v, \",\", [](std::string* out, const std::string& s) {\n out->append(\"\\\"\" + s + \"\\\"\");\n}); // \"\\\"foo\\\",\\\"bar\\\",\\\"baz\\\"\"\n\nabsl::string_view —— 对 C++17 std::string_view 的 backport,这是 Google 早在 C++17 之前就大面积使用的类型。类似函数参数中使用 string_view 而非 const string&,既高效又不受字符串类型限制:\n// 接受 string / char* / string_view,无需隐式构造 string\nvoid Process(absl::string_view input) {\n auto trimmed = absl::StripAsciiWhitespace(input);\n // ...\n}\n\n其他实用工具:absl::StripPrefix、absl::StripSuffix、absl::StrReplaceAll、absl::Substitute(类似 printf 但类型安全)。\n容器:absl/containerSwiss Table 哈希表Abseil 最知名的组件就是 Swiss Table 系列哈希容器——absl::flat_hash_map、absl::flat_hash_set、absl::node_hash_map。\nSwiss Table 是一种开放寻址的哈希表,利用 SIMD 指令并行探测,缓存友好。相比 std::unordered_map,内存占用更低、查找速度更快。在 Google 内部替代了 std::unordered_map 后,整体服务延迟有可测量的下降:\n#include \"absl/container/flat_hash_map.h\"\n\nabsl::flat_hash_map<std::string, int> scores;\nscores[\"alice\"] = 95;\nscores[\"bob\"] = 87;\n\n// 查找性能显著优于 std::unordered_map\nauto it = scores.find(\"alice\");\n\n几点注意:\n\nflat_hash_map 的元素存储在连续内存中,适合 Key/Value 较小、频繁查找的场景\nnode_hash_map 的元素存储在单独节点中,指针稳定性好,适合需要保持元素地址不变的场景\n两种都不保证迭代顺序\n\nbtree 有序容器absl::btree_map、absl::btree_set 基于 B 树实现的有序容器,比 std::map(红黑树)更缓存友好:\n#include \"absl/container/btree_map.h\"\n\nabsl::btree_map<int, std::string> items;\n\nInlinedVectorabsl::InlinedVector<T, N> 是一个小容量优化的 vector——前 N 个元素存储在对象内部(栈上),超过 N 后才分配堆内存。对于大多数情况下元素很少的场景,避免了堆分配:\n// 大多数请求的参数不超过 4 个,不需要分配堆内存\nabsl::InlinedVector<std::string, 4> params;\nparams.push_back(\"key1=val1\");\nparams.push_back(\"key2=val2\");\n\n时间:absl/timeabsl::Time 和 absl::Duration 是对 <chrono> 的实用替代。它们是具体类型而非模板,API 更直观:\n#include \"absl/time/time.h\"\n\nabsl::Time now = absl::Now();\nabsl::Time deadline = now + absl::Seconds(5);\n\nabsl::Duration elapsed = absl::Now() - now;\nint64_t millis = absl::ToInt64Milliseconds(elapsed);\n\n// 时区支持\nabsl::TimeZone tz;\nabsl::LoadTimeZone(\"Asia/Shanghai\", &tz);\nstd::string s = absl::FormatTime(\"%Y-%m-%d %H:%M:%S\", now, tz);\n\n关键点:\n\nabsl::Now() 高度优化,调用极快\n支持 absl::FormatTime / absl::ParseTime 的格式化与解析\nabsl::SleepFor(absl::Seconds(1)) 替代 std::this_thread::sleep_for\n\n同步原语:absl/synchronizationabsl::Mutex 是功能丰富的互斥锁,支持读写锁语义、死锁检测,并且兼容 std::lock_guard:\n#include \"absl/synchronization/mutex.h\"\n\nclass Cache {\npublic:\n std::string Get(const std::string& key) {\n absl::ReaderMutexLock lock(&mu_); // 读锁,允许多个读者并发\n auto it = data_.find(key);\n return it != data_.end() ? it->second : \"\";\n }\n\n void Set(const std::string& key, std::string value) {\n absl::WriterMutexLock lock(&mu_); // 写锁,排他\n data_[key] = std::move(value);\n }\n\nprivate:\n absl::Mutex mu_;\n std::unordered_map<std::string, std::string> data_;\n};\n\n其他同步工具:\n\nabsl::CondVar —— 条件变量,与 absl::Mutex 配合使用\nabsl::Notification —— 一次性通知,用于等待某个事件发生一次\nabsl::Barrier —— 线程屏障,阻塞直到所有线程到达\nabsl::BlockingCounter —— 倒计数锁存器\n\n// Notification 示例:worker 完成后通知 waiter\nabsl::Notification done;\nstd::thread worker([&done]() {\n DoWork();\n done.Notify();\n});\ndone.WaitForNotification(); // 阻塞直到 worker 完成\n\n错误处理:absl/statusabsl::Status 和 absl::StatusOr<T> 提供了一种不依赖异常的、结构化的错误处理方式。这在整个 Google 基础设施中被广泛使用(包括 gRPC、Protobuf 等):\n#include \"absl/status/status.h\"\n#include \"absl/status/statusor.h\"\n\n// 返回 Status,表示成功或失败\nabsl::Status ReadConfig(const std::string& path) {\n if (path.empty()) {\n return absl::InvalidArgumentError(\"path is empty\");\n }\n // ... 读取配置\n return absl::OkStatus();\n}\n\n// StatusOr 要么返回有效值,要么返回错误\nabsl::StatusOr<int> ParseInt(absl::string_view s) {\n int result;\n if (absl::SimpleAtoi(s, &result)) {\n return result;\n }\n return absl::InvalidArgumentError(\"not a valid integer\");\n}\n\n// 使用\nauto result = ParseInt(\"42\");\nif (result.ok()) {\n std::cout << *result << \"\\n\";\n} else {\n std::cerr << result.status().message() << \"\\n\";\n}\n\n随机数:absl/randomAbseil 的随机数库是对 <random> 库的高质量替代,API 更友好,性能更优:\n#include \"absl/random/random.h\"\n\nabsl::BitGen gen; // 高质量的随机数生成器\n\n// 生成均匀分布的整数\nint idx = absl::Uniform(gen, 0, 100);\n\n// 按概率采样\nbool sampled = absl::Bernoulli(gen, 0.1); // 10% 概率为 true\n\n// 随机排列\nstd::vector<int> v = {1, 2, 3, 4, 5};\nstd::shuffle(v.begin(), v.end(), gen);\n\n命令行参数:absl/flagsABSL_FLAG 宏系统支持分布式定义——可以在任何 .cc 文件中定义 flag,会自动汇集到主程序中:\n#include \"absl/flags/flag.h\"\n#include \"absl/flags/parse.h\"\n\n// 在任意 .cc 文件中定义\nABSL_FLAG(std::string, server_addr, \"0.0.0.0:8080\", \"server listen address\");\nABSL_FLAG(int32_t, thread_num, 8, \"worker thread count\");\nABSL_FLAG(bool, enable_cache, true, \"enable local cache\");\n\nint main(int argc, char** argv) {\n absl::ParseCommandLine(argc, argv);\n\n std::string addr = absl::GetFlag(FLAGS_server_addr);\n int threads = absl::GetFlag(FLAGS_thread_num);\n // ...\n}\n\n哈希:absl/hashabsl::Hash<T> 是一个统一的可扩展哈希框架,可以直接对自定义类型进行哈希:\n#include \"absl/hash/hash.h\"\n\nstruct Point {\n int x, y;\n\n template <typename H>\n friend H AbslHashValue(H h, const Point& p) {\n return H::combine(std::move(h), p.x, p.y);\n }\n};\n\n// 现在 Point 可以直接作为 flat_hash_map 的 Key\nabsl::flat_hash_map<Point, std::string> labels;\n\n只需提供 AbslHashValue 友元函数,自定义类型就能用于所有 Abseil 哈希容器。\n数值:absl/numeric提供 128 位整数 absl::int128 / absl::uint128,以及 C++20 位操作的 backport(countl_zero、rotl 等):\n#include \"absl/numeric/int128.h\"\n\nabsl::uint128 hash = absl::MakeUint128(high64, low64);\n\n调试:absl/debugging提供堆栈追踪、符号化、泄漏检测支持:\n\nabsl::GetStackTrace() —— 获取当前调用栈\nabsl::Symbolize() —— 将地址符号化为函数名\nabsl::LeakCheckDisabler —— 在已知泄漏的区域禁用 LeakSanitizer\n\n关键要点1. 使用 absl/strings 代替手写字符串工具StrCat、StrSplit、StrJoin 经过高度优化,比手写循环拼接/拆分可靠得多。尤其是 StrCat,通过预先计算总长度一次分配即可完成拼接,避免了多次重新分配。\n2. 用 Swiss Table 替换 unordered_map如果你在服务中发现 unordered_map 的查找占用了显著比例的 CPU 时间,换成 flat_hash_map 通常能获得 20%-50% 的性能提升。但要注意:\n\n迁移时使用 AbseilHashValue 特化自定义 Key 的哈希\n关注 flat_hash_map 的迭代器失效规则与 unordered_map 不同\n\n3. 用 StatusOr 建立统一的错误处理规范在没有异常的代码库中,Status / StatusOr 提供了一种清晰的错误传播方式。它比返回 pair<Result, error_code> 更安全(编译器会检查是否真正处理了错误),比返回 optional 携带更多错误信息。\n4. 注意 Live at Head 理念对团队的影响Live at Head 意味着要频繁更新 Abseil 版本并跟随 API 变更进行迁移。Google 提供了 clang-tidy 检查和一个迁移工具脚本,可以自动处理大部分 API 变更。如果你的项目无法接受这种节奏,需要额外投入精力做版本锁定和兼容性管理。\n5. 不要引入不必要的组件Abseil 的模块之间独立性较好,按需引入即可。一个常见的错误是「因为引入了 StrCat 而 link 了整个 Abseil」——实际上每个模块都是独立的编译目标,只 link 需要的即可。\n总结Abseil 是 Google 在大规模生产环境中积累的 C++ 最佳实践的沉淀。它的设计务实、性能出色、API 稳定。对于中大型 C++ 项目来说,Abseil 提供的 Swiss Table、字符串工具、StatusOr 错误处理、Mutex 同步原语等都是可以显著提升开发效率和运行时性能的组件。理解其设计理念(补充而非替代、Live at Head、不保证 ABI 稳定性)是正确使用它的前提。\n","categories":["cpp-devel"],"tags":["c++","abseil","基础库","swiss-table"]},{"title":"apollo c++客户端","url":"/posts/bd7510e0.html","content":"简介apollo是携程开源的一款较为成熟的远程配置系统。通过这个远程配置,用户可以在界面上配置参数,然后让参数自动同步到服务上,减少中间不必要的CICD流程。目前apollo提供了java和.net的客户端,但没有c++版本的,本文将设计并实现一款c++语言版本的客户端。对于直接使用的用户,可以直接跳到“使用案例”环节。\n设计痛点配置文件解析一直是c++同学的痛,每次添加配置就需要一段额外的代码解析。而解析的步骤必须要先判断key在不在,然后判断value的类型,接着把value取出来。如果value比较深,就要解析好几回,费时费力不说,中间环节还不能少,一少就容易出coredump(没错,就是rapidjson)。像某些语言就不一样了,直接一键反序列化(比如jackson),还是很方便的。一套方便的反序列化方式,还是很有必要的。\n引入远程配置系统之前,通常配置文件是本地配置,分为一次性加载和热加载两种,前者在服务启动时读取并解析;后者需要开相应的线程区侦测文件变化,文件发生变化后,重新加载解析,并替换内存中的数据。接入远程配置之后,客户端需要知道配置的变更,并同步下来并替换内存中的数据。目前apollo并没有中间者,无法从中间者订阅更新,需要客户端根据已有的接口,实现一套更新机制。\n本地配置加载失败通常是配置文件解析失败,或者磁盘坏了加载不了。前者只要流水线完善基本可以避免,后者虽然避免不了,但是发生的概率极低,并且出现问题的实例也不会很多。引入远程配置系统后,远程配置的服务稳定性影响着接入远程配置的服务的稳定性。虽然这类基础服务的服务稳定性较高,但是没有到达100%,当接入实例较多时,一次重启总会出现几个失败的实例。一套有效的容错机制还是有必要的。\n解决方案反序列化问题目前apollo支持properties、json、yaml、yml、xml、txt等格式,其中json、yaml、yml、xml、txt不支持分级发布。properties支持kv结构,value不支持复杂结构。目前c++中能支持“一键反序列化”的就数protobuf和thrift了,本文将使用protobuf作为反序列化工具。\n上面提到的哪几种不支持分级发布的格式,可以认为是兼容格式,应该逐渐迁移到properties中。properties各种中的value本身是字符串,可以填充json,然后使用protobuf进行反序列化。使用的时候根据key获取对应Message数据,减少解析的环节。这种方式一方面能对配置进行分组管理,避免配置较多时出现杂乱的情况;另一方面,远程配置客户端通常是不保障同一个业务请求中多次调用获取同一个值的。如果配置基础类型,通常在一个业务请求中会使用多次,这时如果每次的值不一样,可能导致异常的情况,并且还不好定位。综上,本文的客户端强制要求value是json格式,然后使用对应的Message进行反序列化。\n更新问题apollo提供了三套接口,带缓存接口、不带缓存接口、long poll接口。\n“带缓存接口”每次请求都会返回相应的数据,需要用户自行判断数据是否更新。一般这个接口是固定事件间隔轮询,查询一次就反序列化数据,并更新内存数据,实际也没啥大的问题,除了看上去比较low之外。\n“不带缓存接口”需要用户每次请求带上releaseKey,然后如果配置更新则返回数据和新的releaseKey,否则返回304表示配置没更新。通过这个接口可以发现数据是否已经更新,方便替换。但是这个接口不知道性能怎么样。\n“long poll”接口,需要客户端请求服务,然后服务把接口挂起,直到配置有更新,才会有相应。这个需要客户端有特殊处理,不然超过60s,自动超时。\n权衡之后,选择第二种“不带缓存接口”,客户端需要额外记录每个namespace的releaseKey,方便后续更新。\n实现使用案例参考","categories":["cpp-devel"],"tags":["c++","apollo","远程配置"]},{"title":"asio_multithread","url":"/posts/aef5c1c6.html","content":"简介boost.asio是boost中的一个基于事件的网络库。本文将介绍asio的多线程模型。asio有两种支持多线程的方案:方案一,开启一个线程池,每个线程独占一个io_context,并在各自的线程中运行io_context的run方法;方案二,开启一个线程池,并创建一个全局的io_context,在每个线程中调用io_context的run方法。备注:新版本的asio使用io_context代替io_servvice。\n多io_context方案在这个多线程方案中,每个线程拥有一个io_context对象,同一个socket不会在多线程中共享,因此不需要引入同步机制。针对io型服务来说,io_context的数量应与cpu数量保持一致;针对计算型服务,请求阻塞了当前线程,当前线程将无法处理其他事件。\n实现接着我们来讲一讲这个方案的实现。为了实现线程池中每个线程拥有一个io_context对象,我们需要先实现一个context池,然后供线程池和其他操作使用。幸运的是,asio代码库的example中提供了这样一个例子。io_context_pool类即是上文提到的context池,其申明如下:\nclass io_context_pool : private boost::noncopyable {\npublic:\n explicit io_context_pool(std::size_t pool_size);\n void run();\n void stop();\n boost::asio::io_context& get_io_context();\n\nprivate:\n typedef boost::shared_ptr<boost::asio::io_context> io_context_ptr;\n typedef boost::asio::executor_work_guard<\n boost::asio::io_context::executor_type> io_context_work;\n\n std::vector<io_context_ptr> io_contexts_;\n\n std::list<io_context_work> work_;\n\n std::size_t next_io_context_;\n};\n\n这个类中提供了三个方法:\n\nrun方法,创建线程池并在每个线程中运行io_context的run方法\nstop方法,停用所有io_context\nget_io_context方法,使用roundrobin算法获取io_context对象\n\n接着io_context_pool类对象作为server类的成员变量,在start_accept函数和构造函数中使用。server类的声明如下: \nclass server : private boost::noncopyable {\npublic:\n explicit server(const std::string& address, const std::string& port, const std::string& doc_root, std::size_t io_context_pool_size);\n void run();\n\nprivate:\n void start_accept();\n void handle_accept(const boost::system::error_code& e);\n void handle_stop();\n io_context_pool io_context_pool_;\n boost::asio::signal_set signals_;\n boost::asio::ip::tcp::acceptor acceptor_;\n connection_ptr new_connection_;\n request_handler request_handler_;\n};\n\n共享io_context方案这种方案先创建一个全局的io_context对象,然后开启线程池,在每个线程中调用io_context的run方法。当出现异步事件时,io_context对象会将事件句柄交付给任意线程进行处理。这时io_context不会被某个事件阻塞,但多个线程共享事件循环可能导致socket描述符被多个线程共享,引起竞态条件,为此需要使用asio提供的strand方法来解决io问题。\n实现这个方案实现起来相对简单,不再需要实现context池,只需要实现一个线程池,并在线程池中执行io_context的run方法即可。在asio库中包含了这个例子,在线程池中运行全局的io_context的run方法。\nserver类的声明如下:\nclass server : private boost::noncopyable {\npublic:\n explicit server(const std::string& address, const std::string& port, const std::string& doc_root, std::size_t thread_pool_size);\n void run();\n\nprivate:\n void start_accept();\n void handle_accept(const boost::system::error_code& e);\n void handle_stop();\n \n std::size_t thread_pool_size_;\n boost::asio::io_context io_context_;\n boost::asio::signal_set signals_;\n boost::asio::ip::tcp::acceptor acceptor_;\n connection_ptr new_connection_;\n request_handler request_handler_;\n};\n从server类的声明中可以看到,全局的io_context对象存储在io_context_变量中,并在run函数和构造函数中进行使用。\n性能比较上面讲了两种方案的实现,这一节做一个简单的性能测试。asio的example提供的两个server实现了基本的http server,本次用于测试的客户端使用brpc的http客户端,进行测试的serer开启六个个线程。这是本次性能测试使用的代码库。测试结果显示,这两种方案的峰值qps都在2w左右,性能差不多。这两个server都会进行文件读写操作,一定程度上影响了测试结果,且本文使用的代码并没有进行优化,这也可能无法正确区分两种方案的性能差异。\n参考\n[The Boost C++ Libraries Chapter 32. Boost.Asio](The Boost C++ Libraries Chapter 32. Boost.Asio)\nA guide to getting started with boost::asio\nStrands: Use Threads Without Explicit Locking\nPost on ASIO strand\nHow strands guarantee correct execution of pending events in boost.asio\nasio C++ libraryUsing Asio with C++11\n\n","categories":["cpp-devel"]},{"title":"Boost.Asio 异步网络编程详解","url":"/posts/9d5b3554.html","content":"简介Boost.Asio 是一个跨平台的 C++ 异步 I/O 库,提供了统一的同步和异步编程模型,支持网络通信、定时器、信号处理、串口通信等。Asio 的设计非常成熟——它曾是 Boost 的一部分,现已被采纳为 C++ 标准提案(C++23 引入 <networking> 库,但目前投票未通过并被搁置,ASIO 的独立版本仍在继续演进)。\nAsio 的核心优势在于:用同一套 API 跨越 Windows(IOCP)、Linux(epoll / io_uring)、macOS/BSD(kqueue)等平台,底层自动选择最优的 I/O 多路复用机制。截至 2025 年,Asio 已全面支持 C++20 协程,使得异步代码可以用同步风格编写,大幅降低了异步编程的复杂度。\nAsio 目前由 C++ 标准委员会成员 Christopher Kohlhoff 独立维护,有 Boost 版本(boost::asio)和独立版本(asio::,header-only),两者 API 几乎一致。\n核心架构Proactor 模式Asio 的异步模型基于 Proactor 设计模式(与 Reactor 相对)。理解这两种模式的区别是理解 Asio 的关键:\n\n\n\n模式\n监控什么\n谁执行 I/O\n通知什么\n\n\n\nReactor\n套接字「可读/可写」就绪\n用户代码读写\n“你可以读了”\n\n\nProactor\nI/O 操作完成\nOS/内核代为执行\n“数据已读到你的缓冲区”\n\n\n在 Proactor 模式中,你发起一个异步操作并提交缓冲区,OS 在后台执行实际的 I/O,完成后通知你。整个过程用户线程不会因 I/O 阻塞。\n平台实现差异\nWindows:使用 IOCP(I/O Completion Ports)——操作系统级别的真正 Proactor\nLinux(epoll 路径):用 epoll 模拟 Proactor——epoll 检测到套接字就绪后,Asio 内部同步执行 I/O,再将完成处理器入队\nLinux(io_uring 路径):较新版本(Boost 1.78+)支持 io_uring,提供了类似 Windows IOCP 的真正的 Proactor 路径\n\n无论底层用什么机制,上层 API 完全一致,这也是 Asio 最核心的价值。\nio_context:调度心脏io_context(旧称 io_service)是 Asio 的核心执行引擎,承担多重角色:\n\nI/O 多路复用器:封装 epoll / IOCP / kqueue,等待 OS 事件通知\n任务队列:维护待执行的完成处理器(completion handler)队列\n定时器调度:基于堆的定时器管理\n通用执行上下文:不止用于 I/O,任何可投递的任务(通过 post / dispatch / defer)都可以在这里执行\n\n关键方法:\n\n\n\n方法\n行为\n\n\n\nrun()\n阻塞运行事件循环,直到没有待处理的处理器\n\n\npoll()\n执行所有就绪的处理器,不阻塞,立即返回\n\n\nrun_one()\n阻塞执行最多一个处理器\n\n\nstop()\n停止事件循环\n\n\nrestart()\n重置 stopped 状态,使 run() 可再次调用\n\n\n防止 run() 过早退出当没有待处理的异步操作时,run() 会返回。为了防止事件循环过早退出,使用 executor_work_guard(旧称 io_context::work):\n// 即使没有 I/O 操作,run() 也会阻塞等待\nauto work = boost::asio::make_work_guard(io_ctx);\nio_ctx.run(); // 不会返回,直到 work 被 reset\n\n异步操作的完整生命周期以一次 async_read 为例,跟踪完整链路:\n1. 发起操作\n async_read(socket, buffer, handler)\n ↓\n2. 向 OS 注册\n epoll_ctl(EPOLLIN) / WSARecv + IOCP\n ↓\n3. 用户线程立即返回(无阻塞)\n ↓\n4. OS 通知数据就绪 / I/O 完成\n ↓\n5. Asio 将 handler 放入 io_context 的任务队列\n ↓\n6. run() 取出 handler 并执行\n\n理解这个流程的关键在于:异步操作不是”在另一个线程执行”,而是”交给 OS 执行,完成后再通知你”。因此你可以用单线程驱动成千上万个连接。\n核心组件I/O 对象Asio 提供了丰富的 I/O 对象,每个都同时支持同步和异步操作:\n\n\n\nI/O 对象\n用途\n\n\n\nip::tcp::socket\nTCP 套接字\n\n\nip::tcp::acceptor\nTCP 服务端监听器\n\n\nip::udp::socket\nUDP 套接字\n\n\nsteady_timer\n单调时钟定时器\n\n\nsystem_timer\n系统时钟定时器\n\n\nserial_port\n串口通信\n\n\nsignal_set\nPOSIX 信号处理\n\n\nlocal::stream_protocol::socket\nUnix Domain Socket\n\n\n同步 vs 异步示例:\n// 同步读取 — 阻塞当前线程\nstd::size_t n = boost::asio::read(socket, buffer);\n\n// 异步读取 — 发起后立即返回,完成时调用 handler\nboost::asio::async_read(socket, buffer, [](error_code ec, std::size_t n) {\n // 处理读取结果\n});\n\n缓冲区Asio 的缓冲区是非拥有型的内存视图,避免无谓的数据拷贝:\n// const_buffer — 只读视图\n// mutable_buffer — 可写视图\n// asio::buffer() — 工厂函数,自动推导类型\n\nstd::string request;\nchar buf[1024];\nstd::vector<char> data(4096);\n\nboost::asio::async_read(socket, boost::asio::buffer(buf), handler);\nboost::asio::async_read(socket, boost::asio::buffer(data), handler);\nboost::asio::async_read(socket, boost::asio::dynamic_buffer(request), handler);\n\n重要规则:异步操作期间,缓冲区必须保持有效。因为 Proactor 模式中,OS 可能直接向你的缓冲区写入数据(Windows IOCP 就是如此)。最常见的模式是用 shared_ptr 保持缓冲区生命周期:\nauto buf = std::make_shared<std::string>(\"hello\");\nasync_write(socket, boost::asio::buffer(*buf),\n [buf](error_code ec, std::size_t n) {\n // buf 通过 shared_ptr 捕获,保持生命周期\n });\n\n定时器steady_timer 基于单调时钟,不受系统时间调整影响:\nboost::asio::steady_timer timer(io_ctx);\n\n// 同步等待\ntimer.expires_after(std::chrono::seconds(5));\ntimer.wait(); // 阻塞 5 秒\n\n// 异步等待\ntimer.async_wait([](error_code ec) {\n if (!ec) std::cout << \"timer fired\\n\";\n});\n\nsteady_timer::expires_after() 每次都重新设置过期时间,而 expires_at() 设置绝对时间点。异步定时器是 Asio 中实现超时控制的基础。\n解析器ip::tcp::resolver 将域名解析为端点列表(阻塞 DNS 查询通常在线程池中执行,避免阻塞事件循环):\nboost::asio::ip::tcp::resolver resolver(io_ctx);\nauto endpoints = resolver.resolve(\"www.example.com\", \"80\");\n// endpoints 是一个迭代器,包含多个解析结果\n\nboost::asio::connect(socket, endpoints); // 自动尝试直到连接成功\n\n并发模型多线程运行 io_context多个线程同时调用同一个 io_context::run() 时,完成处理器会在这些线程间并发执行。Asio 保证处理器入队和出队是线程安全的:\nboost::asio::io_context io_ctx;\n\n// 启动线程池\nstd::vector<std::thread> threads;\nfor (int i = 0; i < std::thread::hardware_concurrency(); ++i) {\n threads.emplace_back([&io_ctx] { io_ctx.run(); });\n}\n\nfor (auto& t : threads) t.join();\n\n陷阱:多线程模式下,同一个 socket 的回调可能在不同线程中并发执行。对每个连接的状态访问需要同步保护。\nStrandstrand 是 Asio 提供的轻量级串行化机制——保证投递到同一个 strand 的处理器按 FIFO 顺序串行执行,无需显式加锁:\n// 方式一:创建 strand\nauto strand = boost::asio::make_strand(io_ctx);\n\n// 方式二:通过 strand 投递\nboost::asio::post(strand, [] { /* 独享连接状态 */ });\n\n// 方式三:bind_executor\nasync_read(socket, buffer,\n boost::asio::bind_executor(strand, handler));\n\n典型用法:为每个 TCP 连接分配一个 strand,这样同一连接的所有回调串行执行,不同连接之间则并行执行。这比每个连接一个 mutex 更高效,因为无需上下文切换和锁竞争。\nC++20 协程支持C++20 协程是 Asio 近年最重大的变革。用 co_await 替代回调链,异步代码以同步风格书写,可读性和可维护性大幅提升。\n基础用法#include <boost/asio.hpp>\n#include <boost/asio/co_spawn.hpp>\n#include <boost/asio/use_awaitable.hpp>\n\nboost::asio::awaitable<void> echo(tcp::socket socket) {\n try {\n char data[1024];\n for (;;) {\n // 异步读取,co_await 挂起协程,不阻塞线程\n std::size_t n = co_await socket.async_read_some(\n boost::asio::buffer(data),\n boost::asio::use_awaitable);\n\n // 异步写回\n co_await boost::asio::async_write(\n socket,\n boost::asio::buffer(data, n),\n boost::asio::use_awaitable);\n }\n }\n catch (std::exception& e) {\n std::cerr << \"echo error: \" << e.what() << \"\\n\";\n }\n}\n\nint main() {\n boost::asio::io_context ctx;\n // ...\n boost::asio::co_spawn(\n ctx.get_executor(),\n echo(std::move(socket)),\n boost::asio::detached // 忽略协程结果\n );\n ctx.run();\n}\n\n关键点:\n\n协程返回类型必须是 boost::asio::awaitable<T>\n给 async_* 函数传入 use_awaitable 作为 Completion Token,使其返回 awaitable\n用 co_spawn 启动协程,第一个参数是 executor,决定协程在哪个线程执行\n异常会被自动传播——async_* 的错误会以 system_error 形式抛到协程内部\n\n错误码风格 vs 异常风格使用 as_tuple(use_awaitable) 获取错误码而非异常:\nconstexpr auto tok = boost::asio::as_tuple(boost::asio::use_awaitable);\n\nboost::asio::awaitable<void> handle(tcp::socket& socket) {\n // 返回 tuple<error_code, size_t>\n auto [ec, n] = co_await socket.async_read_some(\n boost::asio::buffer(data), tok);\n if (ec) {\n // 处理错误,不抛异常\n }\n}\n\n对于 async_wait 这类 handler 签名为 void(error_code) 的操作,as_tuple 解包后直接是 error_code:\nauto [ec] = co_await timer.async_wait(boost::asio::as_tuple(boost::asio::use_awaitable));\n\n并发等待:&& 和 || 运算符需要 #include <boost/asio/experimental/awaitable_operators.hpp>。\n&& — 等待所有操作完成:\nusing namespace boost::asio::experimental::awaitable_operators;\nauto [n1, n2] = co_await (\n async_read(sock1, buf1, use_awaitable) &&\n async_read(sock2, buf2, use_awaitable)\n);\n// 两个读取都完成,结果打包为 tuple\n\n若任一失败,另一个立即被取消。\n|| — 等待任一操作完成(常用于超时):\nstd::variant<std::monostate, std::size_t> result = co_await (\n timer.async_wait(use_awaitable) || // index 0: 超时\n socket.async_read_some(buf, use_awaitable) // index 1: 读取完成\n);\n\nif (result.index() == 0) {\n // 超时处理\n} else {\n auto n = std::get<1>(result);\n // 正常处理\n}\n\n这是实现带超时的异步操作的优雅方式。\nuse_awaitable vs deferred两者都能与 co_await 配合,但有性能差异:\n\n\n\nToken\n返回类型\n开销\n\n\n\nuse_awaitable\nawaitable<T>(创建协程帧)\n较高\n\n\ndeferred\n延迟函数对象(无额外协程帧)\n较低\n\n\ndeferred 性能更好,use_awaitable 适合需要返回/存储 awaitable 对象的场景。\n关键要点1. 使用 shared_ptr 管理连接生命周期异步操作中,回调被执行时,发起该操作的「连接对象」必须仍然存活。最可靠的方式是让连接类继承 enable_shared_from_this,在发起异步操作时捕获 shared_from_this():\nclass Session : public std::enable_shared_from_this<Session> {\npublic:\n void Start() {\n auto self = shared_from_this();\n socket_.async_read_some(boost::asio::buffer(buf_),\n [self](error_code ec, std::size_t n) {\n self->HandleRead(ec, n);\n });\n }\n};\n\n2. 注意缓冲区生命周期异步操作完成前,缓冲区必须有效。不要使用栈上的临时缓冲区发起异步操作——当回调执行时,栈帧早已销毁。使用成员变量或堆分配的缓冲区。\n3. Strand 比 Mutex 更适合 Asio对于每个连接的状态保护,用 strand 串行化回调比加 mutex 更好:无锁竞争、无上下文切换、与 Asio 的调度模型天然契合。\n4. 避免在 handler 中阻塞完成处理器应该快速返回。耗时操作(如磁盘 I/O、同步 DNS 查询)应投递到线程池执行,避免阻塞事件循环。\n5. 优先使用协程如果编译器支持 C++20 协程,新代码优先用 co_await 而非回调链。协程消除了「回调地狱」,错误处理也更自然(try-catch 即可)。但要注意:协程帧的分配开销在某些场景可能成为瓶颈,需要实测。\n6. 独立版本 vs Boost 版本的选择\nasio(独立版):header-only,零依赖,包含 Makefile 和 CMake 支持\nboost::asio:作为 Boost 的一部分发布,依赖 Boost.System 等组件\n\n如果项目已使用 Boost,直接用 boost::asio 即可。如果追求最小依赖,选择独立版本更合适。两者的 API 命名空间不同,但接口几乎一致。\n总结Boost.Asio 是 C++ 异步编程的事实标准。它以 Proactor 模式为核心,通过统一的 API 屏蔽了 Windows IOCP、Linux epoll/io_uring、BSD kqueue 等平台差异。io_context 作为调度中心,配合 strand、线程池提供灵活的并发模型,C++20 协程的引入更是从根本上改变了异步代码的编写方式。无论是开发高性能网络服务,还是需要异步定时器、信号处理、串口通信等场景,Asio 都是 C++ 生态中最成熟的选择。\n参考\nAsio 官方文档(独立版)\nBoost.Asio 文档\nBoost.Asio C++ 网络编程(中文)\nAsio Proactor 模式论文\n\n","categories":["cpp-devel"],"tags":["asio","boost","异步编程","网络编程","协程"]},{"title":"autofdo调研","url":"/posts/1493c0ff.html","content":"autoFDO调研背景传统 FDO(Feedback-Directed Optimization)需要两步编译:先插桩编译出 instrumented 二进制,用它跑 workload 生成 profile,再基于 profile 重新编译。插桩版本运行开销大,且需要维护两套二进制。\nAutoFDO 的解决思路:直接用硬件性能计数器(perf)采集 CPU 采样+Branch 数据,离线转换为 GCC 可消费的 profile 格式,省去插桩编译这一步。核心依赖 Intel LBR(Last Branch Record)和 PEBS 特性。\n安装1. 安装 autofdo 工具git clone https://github.com/google/autofdo.git\ncd autofdo\n./configure\nmake -j$(nproc)\nmake install\n\n依赖项:\n\nautoconf / automake\nlibunwind(栈回溯)\nlibpfm4(perf event 编码)\ngflags / glog(日志与选项)\n\n2. 确保 perf 支持 LBR 和 PEBSperf record -e cycles:pp -b -c 100003 -o /tmp/raw.perf -- ./your_binary\n\n关键参数:\n\n-e cycles:pp:PEBS 精确事件,记录指令地址\n-b:开启 LBR 分支采样\n-c 100003:采样周期(质数避免锁步),需要按场景调优\n\n3. 确保 GCC 版本 >= 5.0gcc --version # 需要 5.0+,支持 -fauto-profile\n\n简单案例以一个微基准测试为例:\n编译(第一阶段:源码编译,带 debuginfo)gcc -O2 -g -fno-omit-frame-pointer -o bench bench.c\n\n收集 profile# 用 perf 采集 LBR + PEBS 数据\nperf record -e cycles:pp -b -c 100003 -o bench.perf.data -- ./bench\n\n# 转换为 GCC auto-profile 格式\ncreate_gcov --binary=./bench --profile=bench.perf.data --gcov=bench.afdo\n\n编译(第二阶段:基于 profile 重编译)gcc -O2 -g -fauto-profile=bench.afdo -o bench.optimized bench.c\n\n此时 GCC 会根据 bench.afdo 中的热点和分支概率,重新决策内联、基本块重排、循环展开等优化。\n复杂案例:多源合并与采样调优实际业务中通常需要在多台机器、多个 workload 上采集后合并,常用的流水线:\n1. 多 workload 分别采集perf record -e cycles:pp -b -c 100003 -o wl1.perf.data -- ./svr --workload=a\nperf record -e cycles:pp -b -c 100003 -o wl2.perf.data -- ./svr --workload=b\nperf record -e cycles:pp -b -c 100003 -o wl3.perf.data -- ./svr --workload=c\n\n2. 转换为文本 profile 后合并create_gcov --binary=./svr --profile=wl1.perf.data --gcov=wl1.afdo --gcov_version=1\ncreate_gcov --binary=./svr --profile=wl2.perf.data --gcov=wl2.afdo --gcov_version=1\ncreate_gcov --binary=./svr --profile=wl3.perf.data --gcov=wl3.afdo --gcov_version=1\n\n# profile_merger 按权重合并多个 profile\nprofile_merger --input=wl1.afdo,wl2.afdo,wl3.afdo \\\n --weight=3,2,1 \\\n --output=merged.afdo\n\n3. 重编译gcc -O2 -g -fauto-profile=merged.afdo -o svr.optimized svr.c\n\n采样参数调优建议\n\n\n参数\n作用\n调优方向\n\n\n\nperf record -c <N>\n采样周期\nN 太小则 overhead 大,太大则 profile 稀疏。通常以 CPU 利用率 <= 2% 为目标\n\n\n-e cycles:pp\nPEBS 精确事件\n用 perf list 查看硬件支持的事件,优先选 :pp 后缀\n\n\nLBR 栈深度\n硬件限制\nIntel Skylake 及之后为 32 条,Haswell/Broadwell 为 16 条。过浅影响间接调用解虚拟化效果\n\n\nProfile 质量检查# 总采样数\ndump_gcov --gcov=merged.afdo | head -20\n\n# 热点函数覆盖率,top 30 热点\ndump_gcov --gcov=merged.afdo | sort -t, -k2 -nr | head -30\n\ndump_gcov 输出格式为 函数名,计数,入口计数,类型,检查:\n\n热点函数的采样计数是否充足(建议 top 函数计数 > 10000)\n关键函数是否都有覆盖\n\n注意事项\ndebuginfo 质量:编译时必须加 -g 且不可 strip。release 用 -g1 或 Debug Fission(-gsplit-dwarf)减少体积。\nLBR 限制:虚拟机环境 LBR 可能不可用(需 host 透传),检查 dmesg | grep -i lbr。\nprofile 时效性:源码改动后 profile 会失效,CI 中需要定期重新采集。\n\n参考\ngcc autoFDO教程\ngcc autoFDO教程 翻译\nautoFDO代码库\ngcc 优化参数\n\n","categories":["cpp-devel"]},{"title":"bazel使用文档","url":"/posts/3ca09cc4.html","content":"简介bazel是google开源的构建系统,支持多种语言、支持云端构建等功能。\nbazel与cmake、autoconf不太一样,bazel进行构建时会启动一个服务,然后bazel命令与这个服务进行交互发送指令,这个服务实际进行编译任务。同时bazel提供了一套远程构建、缓存的接口,实现这套接口就可以制定自己的远程构建系统了。\n环境搭建bazel构建工具依赖java和python,bazel启动的构建服务是java开发的,bazel使用的构建配置是基于python写的,因此这两个组件并不可少。\nbazel安装可以参考bazel提供的安装教程\nbazel支持远程方案,包括远程构建和构建缓存。\nbuildform是其中一种解决方案,buildform基于java编写,分为调度服务和工作服务,支持多分片部署,详细文档可以见buildform的wiki。buildform的readme里有两个服务的简单启动命令,在examples目录下有对应的服务配置,使用对应的配置即可启动。\n上面的环境部署好了之后,添加bazel的配置,bazel配置分为用户级别和项目级别,文件名为.bazel。添加下面内容,并在构建时添加 –config=build 即可实现远程编译:\n# Remote Build Execution requires a strong hash function, such as SHA256.\nstartup --host_jvm_args=-Dbazel.DigestFunction=SHA256\n\n# Depending on how many machines are in the remote execution instance, setting\n# this higher can make builds faster by allowing more jobs to run in parallel.\n# Setting it too high can result in jobs that timeout, however, while waiting\n# for a remote machine to execute them.\nbuild:remote --jobs=8\n\n\n# Set various strategies so that all actions execute remotely. Mixing remote\n# and local execution will lead to errors unless the toolchain and remote\n# machine exactly match the host machine.\nbuild:remote --spawn_strategy=remote\nbuild:remote --strategy=Javac=remote\nbuild:remote --strategy=Closure=remote\nbuild:remote --genrule_strategy=remote\nbuild:remote --define=EXECUTOR=remote\n\n# Enable the remote cache so action results can be shared across machines,\n# developers, and workspaces.\nbuild:remote --remote_cache=localhost:8080\n\n# Enable remote execution so actions are performed on the remote systems.\nbuild:remote --remote_executor=localhost:8080\n\n# Enable encryption.\n#build:remote --tls_enabled=true\n\n# Enforce stricter environment rules, which eliminates some non-hermetic\n# behavior and therefore improves both the remote cache hit rate and the\n# correctness and repeatability of the build.\nbuild:remote --experimental_strict_action_env=true\n\n# Set a higher timeout value, just in case.\nbuild:remote --remote_timeout=3600\n\nbuild:remote --auth_enabled=false\n\n# Since we're testing remote execution, let's make sure it always actually\n# happens.\nbuild:remote --remote_accept_cached=false\ntest --cache_test_results=false\n\ntest --test_output=errors\n\n# TODO(b/77217487): Needed for C++ builds until fix to\n# https://github.com/bazelbuild/bazel/issues/4883 is available.\nbuild --nocheck_visibility\n\nbuild:debug --verbose_failures\nbuild:debug --explain=explain.txt\nbuild:debug --verbose_explanations\nbuild:debug --toolchain_resolution_debug\nbuild:debug --subcommands\n\n在mac上使用bazel构建linux产物的方法的讨论在: https://stackoverflow.com/questions/61567876/bazel-mixing-a-linux-remote-execution-platform-with-a-mac-os-local-platform 目前还没有实现。\nbazel平台的概念可以参考文档Bazel自定义工具链。\n构建简单项目使用复杂依赖管理","categories":["cpp-devel"]},{"title":"boost_learn","url":"/posts/a9053cb5.html","content":"","categories":["cpp-devel"]},{"title":"bthread 底层实现深入分析","url":"/posts/e4420122.html","content":"简介bthread 是 Apache bRPC 框架中的 M:N 用户态线程库,设计目标是以极低的成本支撑百万级并发。它与 Go 的 goroutine 理念相似——将大量用户态任务映射到少量内核线程上执行,避免内核态上下文切换的开销。\nbthread 的上下文切换参考了 boost.context 的设计,用手写汇编直接操作寄存器,切换耗时仅约 100-200 ns,比 pthread 切换快 10 倍以上。bRPC 的高并发能力很大程度上依赖 bthread 的高效调度。\n整体架构bthread 的 M:N 模型由三层结构组成:\nbthread (百万级) → TaskGroup (每核一个) → pthread worker (少量)\n 用户态任务 两级队列 + 内核线程\n (协程栈+上下文) Work Stealing (物理执行单元)\n\n核心数据结构:\n\n\n\n组件\n生命周期\n职责\n\n\n\nTaskControl (TC)\n进程全局唯一\n管理所有 worker 线程,协调全局任务窃取\n\n\nTaskGroup (TG)\n每个 pthread 一个\nworker 调度单元,维护任务队列,管理 bthread 执行\n\n\nTaskMeta (TM)\n每个 bthread 一个\n保存 bthread 的上下文:回调函数、栈、寄存器、状态等\n\n\n上下文切换:汇编级实现上下文切换是 bthread 性能的核心。bthread 用汇编直接操作寄存器,仅保存最少的必要状态,实现了纳秒级的协程切换。\n协程上下文初始化:bthread_make_fcontextmake_fcontext 初始化一个新协程的栈空间,设置好入口函数和执行完毕后跳转的 finish 函数:\nbthread_make_fcontext:\n movq %rdi, %rax ; %rdi = 栈底地址\n andq $-16, %rax ; 16 字节对齐(x86-64 ABI 要求)\n leaq -0x48(%rax), %rax ; 预留 72 字节上下文空间\n movq %rdx, 0x38(%rax) ; 偏移 0x38: 保存入口函数 fn 的地址\n stmxcsr (%rax) ; 偏移 0x00: 保存 MXCSR 浮点状态\n fnstcw 0x4(%rax) ; 偏移 0x04: 保存 FPU 控制字\n leaq finish(%rip), %rcx ; 加载 finish 地址\n movq %rcx, 0x40(%rax) ; 偏移 0x40: 设置返回地址为 finish\n ret ; 返回初始化后的上下文指针\n\n初始化后的栈布局(从高地址到低地址):\n+-----------------+ 高地址(栈底)\n| 未使用空间 |\n+-----------------+\n| finish 地址 | ← 0x40(%rax) — fn 返回后跳转到这里(调用 _exit)\n+-----------------+\n| 入口函数 fn | ← 0x38(%rax) — bthread 要执行的函数指针\n+-----------------+\n| FPU 控制字 | ← 0x04(%rax)\n+-----------------+\n| MXCSR 状态 | ← 0x00(%rax) — context 指针指向这里\n+-----------------+ 低地址(栈顶)\n\n关键在于:栈上预置了 finish 作为返回地址。当 fn 执行完毕执行 ret 指令时,会把 finish 的地址弹出到 %rip,从而自动跳转到 finish → _exit,安全结束协程。\n上下文切换:bthread_jump_fcontext切换的核心流程分为两步:保存当前协程的寄存器到栈上,然后从目标协程的栈上恢复寄存器并跳转。整个过程是一个对称操作:\nbthread_jump_fcontext:\n ; ===== 第一步:保存当前协程的寄存器 =====\n pushq %rbp ; 被调用者保存寄存器\n pushq %rbx\n pushq %r15\n pushq %r14\n pushq %r13\n pushq %r12\n\n leaq -0x8(%rsp), %rsp ; 预留 8 字节给浮点状态\n\n cmp $0, %rcx ; preserve_fpu 参数\n je 1f\n stmxcsr (%rsp) ; 保存 MXCSR\n fnstcw 0x4(%rsp) ; 保存 FPU 控制字\n1:\n movq %rsp, (%rdi) ; 将 %rsp 保存到 from->context\n\n ; ===== 第二步:切换到目标协程的栈 =====\n movq %rsi, %rsp ; 将 to->context 加载到 %rsp(栈切换!)\n\n cmp $0, %rcx ; 恢复浮点状态\n je 2f\n ldmxcsr (%rsp) ; 恢复 MXCSR\n fldcw 0x4(%rsp) ; 恢复 FPU 控制字\n2:\n leaq 0x8(%rsp), %rsp\n\n popq %r12 ; 恢复被调用者保存寄存器(逆序)\n popq %r13\n popq %r14\n popq %r15\n popq %rbx\n popq %rbp\n\n popq %r8 ; 弹出返回地址\n movq %rdx, %rax ; 设置返回值\n movq %rdx, %rdi ; 设置第一个参数\n jmp *%r8 ; 跳转到目标协程!\n\n两种切换场景\n\n\n\n切换到新创建的 bthread\n切换到已运行过的 bthread\n\n\n\n%r8 弹出的内容\nfn 入口函数地址\n上次切换时压入的下一条指令地址\n\n\n执行效果\n开始执行 fn;fn 返回后跳转到 finish\n从上次中断处继续执行\n\n\n性能优化要点\n只保存被调用者保存寄存器:%rbx, %rbp, %r12-%r15,调用者保存寄存器(%rax, %rcx, %rdx, %rsi, %rdi, %r8-%r11)由编译器自动管理,无需保存\n浮点状态按需加载:通过 preserve_fpu 参数控制是否保存/恢复浮点状态。大部分 bthread 任务不涉及浮点运算,跳过这步可以节省数十纳秒\n16 字节栈对齐:符合 x86-64 ABI,保证 SIMD 指令正常执行\n\n调度模型两级任务队列每个 TaskGroup 维护两个队列,设计上刻意区分了「来源」:\n\n\n\n队列\n类型\n用途\n\n\n\n_rq (WorkStealingQueue)\n无锁环形队列\n本 worker 自身创建的 bthread\n\n\n_remote_rq (RemoteTaskQueue)\n带锁队列\n非 worker 线程(主线程等)提交的 bthread\n\n\n关键设计:worker 不从自己的 _rq 取任务——自己的 _rq 只在 ending_sched() → task_runner() 的过程中被消费。wait_task() 只从 _remote_rq 取,或通过 Work Stealing 从其他 worker 的队列窃取。\n这种设计避免了同一个 _rq 被多个 TaskGroup 同时访问产生锁竞争。\n任务获取与 Work StealingWorker 在无任务可执行时,按以下优先级获取新任务:\nwait_task()\n ├── 1. 本 TG._remote_rq.pop() ← 优先级最高,处理外部提交的任务\n ├── 2. _control.steal_task() ← 全局 Work Stealing\n │ ├── 其他 TG._rq.steal() ← 从队尾窃取(无锁 CAS,缓存友好)\n │ └── 其他 TG._remote_rq.pop() ← 窃取外部提交的任务\n └── 3. _pl->wait() ← 所有尝试失败,休眠等待唤醒\n\nWork Stealing 的核心逻辑是随机遍历,配合 offset 步进,避免每次从同一个 worker 窃取产生热点:\nbool TaskControl::steal_task(bthread_t* tid, size_t* seed, size_t offset) {\n for (size_t i = 0; i < ngroup; ++i, *seed += offset) {\n TaskGroup* g = _groups[*seed % ngroup];\n if (g && g->_rq.steal(tid)) // 从队尾窃取,减少 CAS 冲突\n return true;\n if (g && g->_remote_rq.pop(tid))\n return true;\n }\n return false;\n}\n\n窃取队尾而不是队头的原因:push 和 pop 都在队头操作,如果窃取也走队头,会产生频繁的 CAS 竞争。从队尾窃取与 push/pop 错开,降低冲突。\n协作式调度bthread 采用非抢占式调度,只在特定时机主动让出 CPU:\n\n\n\n时机\n行为\n\n\n\nbthread_yield() / bthread_usleep()\n立即切换,挂起当前 bthread\n\n\nbthread_mutex_lock() 等待锁\n在 butex 上挂起,让出 worker\n\n\nI/O 等待(epoll 结合)\nEventDispatcher 检测到事件未就绪时自动 yield\n\n\n阻塞系统调用(如 read())\n整个 worker 被 OS 挂起,其他 worker 偷取该 worker 队列的任务\n\n\n当 bthread 执行阻塞系统调用时,整个 pthread worker 都会被 OS 挂起。这时其他空闲的 worker 通过 Work Stealing 会偷走被阻塞 worker 的队列中的 bthread 继续执行,保证整体吞吐不受影响。\n栈管理每个 bthread 需要独立的栈空间。bthread 通过 StackPool 和 mmap 实现高效的栈管理。\nStackPool:栈空间复用bthread 分配栈时不直接调用 malloc,而是优先从对象池中复用已分配的栈,避免频繁内存分配:\n// 全局栈池,按栈大小分级管理\nvoid* StackPool::alloc() {\n if (!cached_stacks.empty())\n return cached_stacks.pop_back(); // 复用已有栈\n return mmap_for_stack(STACK_SIZE); // 首次分配\n}\n\nvoid StackPool::free(void* stack) {\n cached_stacks.push_back(stack); // 归还池中,下次复用\n}\n\nGuard Page:栈溢出保护每个 bthread 的栈通过 mmap 匿名映射分配,两端设置保护页,栈溢出时立即触发 SIGSEGV,便于定位问题:\nvoid* alloc_stack(size_t stacksize, size_t guardsize) {\n size_t pagesize = getpagesize();\n size_t alloc_size = stacksize + 2 * guardsize * pagesize;\n\n void* base = mmap(NULL, alloc_size, PROT_READ|PROT_WRITE,\n MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);\n\n // 两端设置为不可读写\n mprotect(base, guardsize * pagesize, PROT_NONE); // 低端保护页\n mprotect((char*)base + alloc_size - guardsize * pagesize,\n guardsize * pagesize, PROT_NONE); // 高端保护页\n\n return (char*)base + alloc_size; // 返回栈底(高地址)\n}\n\n栈底在高地址,栈向低地址增长。一旦越界触碰保护页,立即 core dump,不会静默地破坏相邻内存。\n栈大小分级bthread 通常提供小/中/大三种栈规格,用户可以根据 bthread 的实际需要选择,减少内存浪费。对于只做简单转发的 bthread,几 KB 的栈就已足够,无需默认分配数百 KB。\n同步原语:Butex为什么需要 Butex在 bthread 中如果直接用 pthread_mutex,锁等待时被挂起的是整个 pthread worker,导致该 worker 无法调度其他 bthread,严重影响并发能力。Butex 的设计目标是:在 bthread 等待时只挂起这个 bthread,让 pthread worker 继续执行其他 bthread。\nButex 的设计借鉴了 Linux 内核的 futex 机制:\npthread : futex = bthread : butex\n\nButex 核心结构struct Butex {\n butil::atomic<int> value; // 用户态原子变量(锁状态)\n ButexWaiterList waiters; // 等待者链表(侵入式双向链表)\n FastPthreadMutex waiter_lock; // 保护等待队列的 pthread 锁\n};\n\nstruct ButexBthreadWaiter : public ButexWaiter {\n TaskMeta* task_meta; // 当前 bthread 的 TaskMeta\n TimerThread::TaskId sleep_id; // 超时定时任务 ID\n WaiterState waiter_state; // 等待状态\n int expected_value; // 挂起前期望的 value 值\n TaskControl* control; // 全局 TaskControl\n};\n\nbutex_wait:挂起当前 bthreadint butex_wait(void* arg, int expected_value, const timespec* abstime) {\n Butex* b = container_of(static_cast<butil::atomic<int>*>(arg), Butex, value);\n\n // 快速检查:value 不等于期望值,直接返回(已被修改,不需等待)\n if (b->value.load(memory_order_relaxed) != expected_value) {\n return -1; // EWOULDBLOCK\n }\n\n TaskGroup* g = tls_task_group;\n if (g == NULL || g->is_current_pthread_task()) {\n // 当前是 pthread(非 worker 线程),走 pthread 挂起逻辑\n return butex_wait_from_pthread(g, b, expected_value, abstime);\n }\n\n // 当前是 bthread:在 bthread 栈上创建 waiter\n ButexBthreadWaiter bbw;\n bbw.tid = g->current_tid();\n bbw.expected_value = expected_value;\n\n // 设置超时\n if (abstime != NULL)\n bbw.sleep_id = get_global_timer_thread()->schedule(...);\n\n // 注册 remain 回调:在 worker 切换前原子地完成「检查 + 入队」\n g->set_remained(wait_for_butex, &bbw);\n\n // 让出 CPU,切换到其他 bthread\n g->yield(&bbw.task_meta->tid);\n\n // 被唤醒后从这里继续执行\n unsleep_if_necessary(&bbw, get_global_timer_thread());\n return 0;\n}\n\n防止信号丢失Butex 实现中最关键的细节是防止唤醒信号丢失。如果「检查 value」和「将 waiter 入队」分别在两个临界区内完成,中间存在窗口:另一个线程可能在检查之后、入队之前调用 butex_wake,导致唤醒信号丢失。\nButex 的解决方案是 wait_for_butex——在持有 waiter_lock 的临界区内原子地完成检查和入队:\nvoid wait_for_butex(void* arg) {\n ButexBthreadWaiter* bw = static_cast<ButexBthreadWaiter*>(arg);\n\n std::lock_guard<FastPthreadMutex> guard(bw->initial_butex->waiter_lock);\n\n if (bw->initial_butex->value.load() != bw->expected_value) {\n // value 已变化!不挂起,重新加入就绪队列\n bw->control->ready_to_run(bw->task_meta);\n } else {\n // value 未变,安全地将 waiter 加入等待队列\n bw->container.store(bw->initial_butex);\n bw->initial_butex->waiters.push_back(bw);\n }\n}\n\nwait_for_butex 作为 remain 回调,在 worker 切换到下一个 bthread 之前执行。它持有 waiter_lock,保证与 butex_wake 的互斥,杜绝唤醒丢失。\nbutex_wake:唤醒等待者int butex_wake(void* arg) {\n Butex* b = container_of(static_cast<butil::atomic<int>*>(arg), Butex, value);\n ButexWaiter* front = NULL;\n {\n std::lock_guard<FastPthreadMutex> guard(b->waiter_lock);\n if (!b->waiters.empty()) {\n front = b->waiters.pop_front();\n front->container.store(NULL);\n }\n }\n\n if (front == NULL) return 0;\n\n if (front->tid == 0) {\n // 唤醒的是 pthread\n wakeup_pthread(static_cast<ButexPthreadWaiter*>(front));\n } else {\n // 唤醒的是 bthread:加入就绪队列\n ButexBthreadWaiter* bbw = static_cast<ButexBthreadWaiter*>(front);\n unsleep_if_necessary(bbw, get_global_timer_thread());\n\n // 调用者是 worker 线程:直接放入本地队列(nosignal=true,避免通知已有 worker 自身)\n // 调用者是非 worker:放入 remote_rq\n if (current_worker)\n current_worker->ready_to_run(bbw->task_meta, true);\n else\n bbw->control->ready_to_run_remote(bbw->task_meta);\n }\n return 1;\n}\n\nbthread::Mutex 与 ConditionVariablebthread::Mutex 和 bthread::ConditionVariable 是 Butex 的上层封装,接口与 std::mutex / std::condition_variable 兼容,但挂起的是 bthread 而非 pthread:\nclass Mutex {\n butil::atomic<int> _butex; // 底层基于 butex value\n\n void lock() {\n // 快速路径:CAS 获取锁\n if (_butex.exchange(1, memory_order_acquire) == 0)\n return;\n // 慢速路径:在 butex 上等待\n butex_wait(&_butex, 1, NULL);\n }\n\n void unlock() {\n _butex.store(0, memory_order_release);\n butex_wake(&_butex); // 唤醒一个等待者\n }\n};\n\n整体同步体系的分层结构:\nbthread::Mutex / bthread::ConditionVariable ← 对外接口\nbutex (butex_wait / butex_wake / wake_all) ← bthread 粒度挂起/唤醒\nFastPthreadMutex (futex_wait / futex_wake) ← pthread 粒度(保护 waiter 队列)\nLinux futex 系统调用 ← 内核态(竞争激烈时陷入)\n\n关键要点1. 上下文切换是 bthread 的性能基石仅保存 6 个被调用者保存寄存器 + 可选的浮点状态,浮点状态按需加载,切换耗时 100-200ns。这是比 pthread(1-5μs)快 10 倍以上的根本原因。\n2. Work Stealing 从队尾窃取push/pop 在队头,窃取在队尾——错开操作位置,减少 CAS 竞争,同时提升了 CPU 缓存命中率。\n3. 防止 Butex 信号丢失「检查 value + 入队」必须在同一临界区内原子完成。Butex 通过 remain 回调机制在持有 waiter_lock 时执行这两个操作,与 butex_wake 互斥,保证不会丢失唤醒。\n4. 阻塞系统调用会拖垮整个 workerbthread 是协作式调度,如果 bthread 中执行了阻塞的系统调用(如同步 read()),整个 pthread worker 会被 OS 挂起。但其他 worker 会通过 Work Stealing 偷走被阻塞 worker 的任务,不会造成全局阻塞。最佳实践是:在 bthread 中始终使用异步 I/O,避免直接调用阻塞系统调用。\n5. 栈溢出有 Guard Page 兜底bthread 栈通过 mmap + mprotect 设置保护页,栈溢出时立即触发 SIGSEGV 产生 core dump,不会静默破坏相邻内存。定位问题时可以直接从 core 文件看到溢出位置。\n总结bthread 通过手写汇编实现寄存器级的上下文切换(~100ns),结合 M:N 调度和 Work Stealing 负载均衡,在用户态支撑百万级并发任务。Butex 提供了 bthread 粒度的同步原语,确保锁等待只挂起 bthread 而非整个 pthread。这些设计共同构成了 bRPC 高性能的基石。\n参考\nbRPC 源码\nbRPC v1.7 中文文档 — bthread 机制\nboost.context 文档\n\n","categories":["cpp-devel"],"tags":["协程","brpc","bthread","用户态线程","上下文切换"]},{"title":"c++语法面试题总结","url":"/posts/ad9c07df.html","content":"简介回顾一下c++语法的八股文,来自某博客和leetcode。\n编译内存相关C++ 程序编译过程编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接。\n\n编译预处理:处理以 # 开头的指令;\n编译、优化:将源码 .cpp 文件翻译成 .s 汇编代码;\n汇编:将汇编代码 .s 翻译成机器指令 .o 文件;\n链接:汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个 .cpp文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序 .exe文件。\n\n\n链接分为两种:\n\n静态链接:代码从其所在的静态链接库中拷贝到最终的可执行程序中,在该程序被执行时,这些代码会被装入到该进程的虚拟地址空间中。\n动态链接:代码被放到动态链接库或共享对象的某个目标文件中,链接程序只是在最终的可执行程序中记录了共享对象的名字等一些信息。在程序执行时,动态链接库的全部内容会被映射到运行时相应进行的虚拟地址的空间。\n\n二者的优缺点:\n\n静态链接:浪费空间,每个可执行程序都会有目标文件的一个副本,这样如果目标文件进行了更新操作,就需要重新进行编译链接生成可执行程序(更新困难);优点就是执行的时候运行速度快,因为可执行程序具备了程序运行的所有内容。\n动态链接:节省内存、更新方便,但是动态链接是在程序运行时,每次执行都需要链接,相比静态链接会有一定的性能损失。\n\nC++ 内存管理C++ 内存分区:栈、堆、全局/静态存储区、常量存储区、代码区。\n\n栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。\n堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。\n全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在.bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。\n常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束自动释放。\n代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。\n\n栈和堆的区别\n申请方式:栈是系统自动分配,堆是程序员主动申请。\n申请后系统响应:分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败栈溢出;申请堆空间,堆在内存中呈现的方式类似于链表(记录空闲地址空间的链表),在链表上寻找第一个大于申请空间的节点分配给程序,将该节点从链表中删除,大多数系统中该块空间的首地址存放的是本次分配空间的大小,便于释放,将该块空间上的剩余空间再次连接在空闲链表上。\n栈在内存中是连续的一块空间(向低地址扩展)最大容量是系统预定好的,堆在内存中的空间(向高地址扩展)是不连续的。\n申请效率:栈是有系统自动分配,申请效率高,但程序员无法控制;堆是由程序员主动申请,效率低,使用起来方便但是容易产生碎片。\n存放的内容:栈中存放的是局部变量,函数的参数;堆中存放的内容由程序员控制。\n\n变量的区别全局变量、局部变量、静态全局变量、静态局部变量的区别:\nC++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。\n从作用域看:\n\n全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。\n静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。\n局部变量:具有局部作用域。它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。\n静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。\n\n从分配内存空间看:\n\n静态存储区:全局变量,静态局部变量,静态全局变量。\n栈:局部变量。\n\n全局变量定义在头文件中有什么问题?如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能再头文件中定义全局变量。\n内存对齐什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有什么优点?\n内存对齐:编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中内存对齐的原则:\n\n结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;\n结构体每个成员相对于结构体首地址的偏移量 (offset)都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding);\n结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节(trailing padding)。\n\n进行内存对齐的原因:(主要是硬件设备方面的问题)\n\n某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;\n某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;\n相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;\n某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignmenttrap);\n某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。\n\n内存对齐的优点:\n\n便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;\n提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。\n\n什么是内存泄露内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存。进一步解释:\n\n并非指内存从物理上消失,而是指程序在运行过程中,由于疏忽或错误而失去了对该内存的控制,从而造成了内存的浪费。\n常指堆内存泄漏,因为堆是动态分配的,而且是用户来控制的,如果使用不当,会产生内存泄漏。\n使用 malloc、calloc、realloc、new 等分配内存时,使用完后要调用相应的 free 或 delete释放内存,否则这块内存就会造成内存泄漏。\n指针重新赋值\n\nchar *p = (char *)malloc(10);\nchar *p1 = (char *)malloc(10);\np = np;\n\n开始时,指针 p 和 p1 分别指向一块内存空间,但指针 p 被重新赋值,导致 p 初始时指向的那块内存空间无法找到,从而发生了内存泄漏。\n怎么防止内存泄漏?内存泄漏检测工具的原理?防止内存泄漏的方法:\n\n内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。(说明:但这样做并不是最佳的做法,在类的对象复制时,程序会出现同一块内存空间释放两次的情况)\n智能指针:智能指针是 C++ 中已经对内存泄漏封装好了一个工具,可以直接拿来使用,将在下一个问题中对智能指针进行详细的解释。\n\n内存泄漏检测工具的实现原理:内存检测工具有很多,这里重点介绍下 valgrind 。\n智能指针有哪几种?智能指针的实现原理?智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的。C++11 中封装在了 < memory > 头文件中。\nC++11 中智能指针包括以下三种:\n\n共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。\n独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。\n弱指针(weak_ptr):指向 shared_ptr 指向的对象,能够解决由shared_ptr带来的循环引用问题。\n\n智能指针的实现原理: 计数原理。示例代码如下(实际实现中计数器使用atomic):\n#include <iostream>\n#include <memory>\n\ntemplate <typename T>\nclass SmartPtr {\nprivate : \n\tT *_ptr;\n\tsize_t *_count;\n\npublic:\n\tSmartPtr(T *ptr = nullptr) : _ptr(ptr) {\n\t\tif (_ptr) {\n\t\t\t_count = new size_t(1);\n\t\t} else {\n\t\t\t_count = new size_t(0);\n\t\t}\n\t}\n\n\t~SmartPtr() {\n\t\t(*this->_count)--;\n\t\tif (*this->_count == 0) {\n\t\t\tdelete this->_ptr;\n\t\t\tdelete this->_count;\n\t\t}\n\t}\n\n // 拷贝构造:计数 +1\n\tSmartPtr(const SmartPtr &ptr) {\n\t\tif (this != &ptr) {\n\t\t\tthis->_ptr = ptr._ptr;\n\t\t\tthis->_count = ptr._count;\n\t\t\t(*this->_count)++;\n\t\t}\n\t}\n\n // 赋值运算符重载 \n\tSmartPtr &operator=(const SmartPtr &ptr) {\n\t\tif (this->_ptr == ptr._ptr) {\n\t\t\treturn *this;\n\t\t}\n // 将当前的 ptr 指向的原来的空间的计数 -1\n\t\tif (this->_ptr) {\n\t\t\t(*this->_count)--;\n\t\t\tif (this->_count == 0) {\n\t\t\t\tdelete this->_ptr;\n\t\t\t\tdelete this->_count;\n\t\t\t}\n\t\t}\n\t\tthis->_ptr = ptr._ptr;\n\t\tthis->_count = ptr._count;\n\t\t(*this->_count)++; // 此时 ptr 指向了新赋值的空间,该空间的计数 +1\n\t\treturn *this;\n\t}\n\n\tT &operator*() {\n\t\tassert(this->_ptr == nullptr);\n\t\treturn *(this->_ptr);\n\t}\n\n\tT *operator->() {\n\t\tassert(this->_ptr == nullptr);\n\t\treturn this->_ptr;\n\t}\n\n\tsize_t use_count() {\n\t\treturn *this->count;\n\t}\n};\n\n一个 unique_ptr 怎么赋值给另一个 unique_ptr 对象?借助 std::move() 可以实现将一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,其目的是实现所有权的转移。也可以让当前unique_ptr调用release释放对ptr控制权,然后在另一个unique_ptr获取控制权。\n// A 作为一个类 \nstd::unique_ptr<A> ptr1(new A());\nstd::unique_ptr<A> ptr2 = std::move(ptr1);\nstd::unique_ptr<A> ptr3(ptr2.release());\n\n使用智能指针会出现什么问题?怎么解决?智能指针可能出现的问题:循环引用\n在如下例子中定义了两个类 Parent、Child,在两个类中分别定义另一个类的对象的共享指针,由于在程序结束后,两个指针相互指向对方的内存空间,导致内存无法释放。\n#include <iostream>\n#include <memory>\n\nusing namespace std;\n\nclass Child;\nclass Parent;\n\nclass Parent {\nprivate:\n shared_ptr<Child> ChildPtr;\npublic:\n void setChild(shared_ptr<Child> child) {\n this->ChildPtr = child;\n }\n\n void doSomething() {\n if (this->ChildPtr.use_count()) {\n\n }\n }\n\n ~Parent() {\n }\n};\n\nclass Child {\nprivate:\n shared_ptr<Parent> ParentPtr;\npublic:\n void setPartent(shared_ptr<Parent> parent) {\n this->ParentPtr = parent;\n }\n void doSomething() {\n if (this->ParentPtr.use_count()) {\n\n }\n }\n ~Child() {\n }\n};\n\nint main() {\n weak_ptr<Parent> wpp;\n weak_ptr<Child> wpc;\n {\n shared_ptr<Parent> p(new Parent);\n shared_ptr<Child> c(new Child);\n p->setChild(c);\n c->setPartent(p);\n wpp = p;\n wpc = c;\n cout << p.use_count() << endl; // 2\n cout << c.use_count() << endl; // 2\n }\n cout << wpp.use_count() << endl; // 1\n cout << wpc.use_count() << endl; // 1\n return 0;\n}\n\n循环引用的解决方法: weak_ptr\n循环引用:该被调用的析构函数没有被调用,从而出现了内存泄漏。\n\nweak_ptr 对被 shared_ptr 管理的对象存在非拥有性(弱)引用,在访问所引用的对象前必须先转化为 shared_ptr;\nweak_ptr 用来打断 shared_ptr 所管理对象的循环引用问题,若这种环被孤立(没有指向环中的外部共享指针),shared_ptr 引用计数无法抵达 0,内存被泄露;令环中的指针之一为弱指针可以避免该情况;\nweak_ptr 用来表达临时所有权的概念,当某个对象只有存在时才需要被访问,而且随时可能被他人删除,可以用 weak_ptr 跟踪该对象;需要获得所有权时将其转化为 shared_ptr,此时如果原来的 shared_ptr 被销毁,则该对象的生命期被延长至这个临时的 shared_ptr 同样被销毁。\n\n#include <iostream>\n#include <memory>\n\nusing namespace std;\n\nclass Child;\nclass Parent;\n\nclass Parent {\nprivate:\n //shared_ptr<Child> ChildPtr;\n weak_ptr<Child> ChildPtr;\npublic:\n void setChild(shared_ptr<Child> child) {\n this->ChildPtr = child;\n }\n\n void doSomething() {\n //new shared_ptr\n if (this->ChildPtr.lock()) {\n\n }\n }\n\n ~Parent() {\n }\n};\n\nclass Child {\nprivate:\n shared_ptr<Parent> ParentPtr;\npublic:\n void setPartent(shared_ptr<Parent> parent) {\n this->ParentPtr = parent;\n }\n void doSomething() {\n if (this->ParentPtr.use_count()) {\n\n }\n }\n ~Child() {\n }\n};\n\nint main() {\n weak_ptr<Parent> wpp;\n weak_ptr<Child> wpc;\n {\n shared_ptr<Parent> p(new Parent);\n shared_ptr<Child> c(new Child);\n p->setChild(c);\n c->setPartent(p);\n wpp = p;\n wpc = c;\n cout << p.use_count() << endl; // 2\n cout << c.use_count() << endl; // 1\n }\n cout << wpp.use_count() << endl; // 0\n cout << wpc.use_count() << endl; // 0\n return 0;\n}\n\nshared_from_this使用当成员函数中需要将当前类对象的指针传递给其他对象A时,由于对象A的生命周期未知,直接传this指针可能造成非预期结果。为了解决这个问题,c++从boost中吸收了enable_shared_from_this类,示例代码:\n#include <stdio.h>\n#include <stdlib.h>\n#include <iostream>\n#include <memory>\n \nusing namespace std;\n \nclass TestA : public enable_shared_from_this<TestA>\n{\npublic:\n\tTestA()\n\t{\n\t\tcout << "TestA create" << endl;\n\t}\n\t~TestA()\n\t{\n\t\tcout << "TesA destory" << endl;\n\t}\n \n\tshared_ptr<TestA> getSharedFromThis() { return shared_from_this(); }\n \n};\n \nint main(){\n \n\t{//出了此作用域 ptr1 ptr2 销毁, TestA对象销毁\n\t\tshared_ptr<TestA> ptr1(new TestA());\n\t\tshared_ptr<TestA> ptr2 = ptr1->getSharedFromThis();\n\t\tcout << "ptr1 count: " << ptr1.use_count() << " ptr2 count: " << ptr2.use_count() << endl;\n\t\t//输出:ptr1 count: 2 ptr2 count: 2 可以正常释放对象\n\t}\n \n\treturn 0;\n}\n当然,这种用法在boost的asio中使用比较广泛。\nc++11新特性auto 类型推导auto 关键字:自动类型推导,编译器会在 编译期间 通过初始值推导出变量的类型,通过 auto 定义的变量必须有初始值。auto 关键字基本的使用语法如下:\ndecltype 类型推导decltype 关键字:decltype 是“declare type”的缩写,译为“声明类型”。和 auto 的功能一样,都用来在编译时期进行自动类型推导。如果希望从表达式中推断出要定义的变量的类型,但是不想用该表达式的值初始化变量,这时就不能再用 auto。decltype 作用是选择并返回操作数的数据类型。\n区别:\nauto var = val1 + val2; \ndecltype(val1 + val2) var1 = 0; \n\n\nauto 根据 = 右边的初始值 val1 + val2 推导出变量的类型,并将该初始值赋值给变量 var;decltype 根据 val1 + val2 表达式推导出变量的类型,变量的初始值和与表达式的值无关。\nauto 要求变量必须初始化,因为它是根据初始化的值推导出变量的类型,而 decltype 不要求,定义变量的时候可初始化也可以不初始化。\n\nlambda 表达式lambda 表达式,又被称为 lambda 函数或者 lambda 匿名函数。\nlambda匿名函数的定义:\n[capture list] (parameter list) -> return type\n{\n function body;\n};\n\n其中:\n\ncapture list:捕获列表,指 lambda 所在函数中定义的局部变量的列表,通常为空。\nreturn type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样。\n\n#include <iostream>\n#include <algorithm>\nusing namespace std;\n\nint main()\n{\n int arr[4] = {4, 2, 3, 1};\n //对 a 数组中的元素进行升序排序\n sort(arr, arr+4, [=](int x, int y) -> bool{ return x < y; } );\n for(int n : arr){\n cout << n << " ";\n }\n return 0;\n}\n\n范围 for 语句for (declaration : expression){\n statement\n}\n\n参数的含义:\n\nexpression:必须是一个序列,例如用花括号括起来的初始值列表、数组、vector ,string等,这些类型的共同特点是拥有能返回迭代器的 beign、end 成员。\ndeclaration:此处定义一个变量,序列中的每一个元素都能转化成该变量的类型,常用 auto 类型说明符。\n\n右值引用右值引用:绑定到右值的引用,用 && 来获得右值引用,右值引用只能绑定到要销毁的对象。为了和右值引用区分开,常规的引用称为左值引用。\n#include <iostream>\n#include <vector>\nusing namespace std;\nint main()\n{\n int var = 42;\n int &l_var = var;\n int &&r_var = var; // error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int' 错误:不能将右值引用绑定到左值上\n\n int &&r_var2 = var + 40; // 正确:将 r_var2 绑定到求和结果上\n return 0;\n}\n\n标准库 move() 函数move() 函数:通过该函数可获得绑定到左值上的右值引用,该函数包括在 utility 头文件中。该知识点会在后续的章节中做详细的说明。\n智能指针上面已经详述\ndelete 函数和 default 函数\ndelete 函数:= delete 表示该函数不能被调用。\ndefault 函数:= default 表示编译器生成默认的函数,例如:生成默认的构造函数。\n\n#include <iostream>\nusing namespace std;\n\nclass A\n{\npublic:\n\tA() = default; // 表示使用默认的构造函数\n\t~A() = default;\t// 表示使用默认的析构函数\n\tA(const A &) = delete; // 表示类的对象禁止拷贝构造\n\tA &operator=(const A &) = delete; // 表示类的对象禁止拷贝赋值\n};\nint main()\n{\n\tA ex1;\n\tA ex2 = ex1; // error: use of deleted function 'A::A(const A&)'\n\tA ex3;\n\tex3 = ex1; // error: use of deleted function 'A& A::operator=(const A&)'\n\treturn 0;\n}\n\n\n面向对象什么是面向对象?面向对象的三大特性面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。\n面向对象的三大特性:\n\n封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。\n继承:子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。\n多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。\n\n重载、重写、隐藏的区别重载:是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。\nclass A\n{\npublic:\n void fun(int tmp);\n void fun(float tmp); // 重载 参数类型不同(相对于上一个函数)\n void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数)\n void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数)\n int fun(int tmp); // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型\n};\n\n隐藏:是指派生类的函数屏蔽了与其同名的基类函数,主要只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。\n#include <iostream>\nusing namespace std;\n\nclass Base\n{\npublic:\n void fun(int tmp, float tmp1) { cout << "Base::fun(int tmp, float tmp1)" << endl; }\n};\n\nclass Derive : public Base\n{\npublic:\n void fun(int tmp) { cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数\n};\n\nint main()\n{\n Derive ex;\n ex.fun(1); // Derive::fun(int tmp)\n ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided\n return 0;\n}\n\n说明:上述代码中 ex.fun(1, 0.01); 出现错误,说明派生类中将基类的同名函数隐藏了。若是想调用基类中的同名函数,可以加上类型名指明 ex.Base::fun(1, 0.01);,这样就可以调用基类中的同名函数。\n重写(覆盖):是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。\n#include <iostream>\nusing namespace std;\n\nclass Base\n{\npublic:\n virtual void fun(int tmp) { cout << "Base::fun(int tmp) : " << tmp << endl; }\n};\n\nclass Derived : public Base\n{\npublic:\n virtual void fun(int tmp) { cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数\n};\nint main()\n{\n Base *p = new Derived();\n p->fun(3); // Derived::fun(int) : 3\n return 0;\n}\n\n重写和重载的区别:\n\n范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。\n参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。\nvirtual 关键字:重写的函数基类中必须有 virtual关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。\n\n隐藏和重写,重载的区别:\n\n范围区别:隐藏与重载范围不同,隐藏发生在不同类中。\n参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,基类函数都是被隐藏,而不是重写。\n\n如何理解 C++ 是面向对象编程说明:该问题最好结合自己的项目经历进行展开解释,或举一些恰当的例子,同时对比下面向过程编程。\n\n面向过程编程:一种以执行程序操作的过程或函数为中心编写软件的方法。程序的数据通常存储在变量中,与这些过程是分开的。所以必须将变量传递给需要使用它们的函数。缺点:随着程序变得越来越复杂,程序数据与运行代码的分离可能会导致问题。例如,程序的规范经常会发生变化,从而需要更改数据的格式或数据结构的设计。当数据结构发生变化时,对数据进行操作的代码也必须更改为接受新的格式。查找需要更改的所有代码会为程序员带来额外的工作,并增加了使代码出现错误的机会。\n面向对象编程(Object-Oriented Programming, OOP):以创建和使用对象为中心。一个对象(Object)就是一个软件实体,它将数据和程序在一个单元中组合起来。对象的数据项,也称为其属性,存储在成员变量中。对象执行的过程被称为其成员函数。将对象的数据和过程绑定在一起则被称为封装。\n\n面向对象编程进一步说明:\n面向对象编程将数据成员和成员函数封装到一个类中,并声明数据成员和成员函数的访问级别(public、private、protected),以便控制类对象对数据成员和函数的访问,对数据成员起到一定的保护作用。而且在类的对象调用成员函数时,只需知道成员函数的名、参数列表以及返回值类型即可,无需了解其函数的实现原理。当类内部的数据成员或者成员函数发生改变时,不影响类外部的代码。\n什么是多态?多态如何实现?多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。\n实现方法:多态是通过虚函数实现的,虚函数的地址保存在虚函数表中,虚函数表的地址保存在含有虚函数的类的实例对象的内存空间中。\n实现过程:\n\n在类中用 virtual 关键字声明的函数叫做虚函数;\n存在虚函数的类都有一个虚函数表,当创建一个该类的对象时,该对象有一个指向虚函数表的虚表指针(虚函数表和类对应的,虚表指针是和对象对应);\n当基类指针指向派生类对象,基类指针调用虚函数时,基类指针指向派生类的虚表指针,由于该虚表指针指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数。\n\n#include <iostream>\nusing namespace std;\n\nclass Base\n{\npublic:\n\tvirtual void fun() { cout << "Base::fun()" << endl; }\n\n\tvirtual void fun1() { cout << "Base::fun1()" << endl; }\n\n\tvirtual void fun2() { cout << "Base::fun2()" << endl; }\n};\nclass Derive : public Base\n{\npublic:\n\tvoid fun() { cout << "Derive::fun()" << endl; }\n\n\tvirtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }\n\n\tvirtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }\n};\nint main()\n{\n\tBase *p = new Derive();\n\tp->fun(); // Derive::fun() 调用派生类中的虚函数\n\treturn 0;\n}\n\n基类的虚函数表如下:\n\n派生类的对象虚函数表如下:\n\n简单解释:当基类的指针指向派生类的对象时,通过派生类的对象的虚表指针找到虚函数表(派生类的对象虚函数表),进而找到相应的虚函数 Derive::f() 进行调用。\n关键字库函数sizeof 和 strlen 的区别strlen 是头文件 中的函数,sizeof 是 C++ 中的运算符。strlen 测量的是字符串的实际长度(其源代码如下),以 \\0 结束。而 sizeof 测量的是字符数组的分配大小。\nstrlen 源代码:\nsize_t strlen(const char *str) {\n size_t length = 0;\n while (*str++)\n ++length;\n return length;\n}\n\n#include <iostream>\n#include <cstring>\n\nusing namespace std;\n\nint main()\n{\n char arr[10] = "hello";\n cout << strlen(arr) << endl; // 5\n cout << sizeof(arr) << endl; // 10\n return 0;\n}\n\n若字符数组 arr 作为函数的形参,sizeof(arr) 中 arr 被当作字符指针来处理,strlen(arr) 中 arr 依然是字符数组,从下述程序的运行结果中就可以看出。\n#include <iostream>\n#include <cstring>\n\nusing namespace std;\n\nvoid size_of(char arr[])\n{\n cout << sizeof(arr) << endl; // warning: 'sizeof' on array function parameter 'arr' will return size of 'char*' .\n cout << strlen(arr) << endl; \n}\n\nint main()\n{\n char arr[20] = "hello";\n size_of(arr); \n return 0;\n}\n/*\n输出结果:\n8\n5\n*/\n\nstrlen 本身是库函数,因此在程序运行过程中,计算长度;而 sizeof 在编译时,计算长度;sizeof 的参数可以是类型,也可以是变量;strlen 的参数必须是 char* 类型的变量。\nlambda 表达式(匿名函数)的具体应用和使用场景lambda 表达式的定义形式如下:\n[capture list] (parameter list) -> reurn type\n{\n function body\n}\n\n其中:\n\ncapture list:捕获列表,指 lambda 表达式所在函数中定义的局部变量的列表,通常为空,但如果函数体中用到了 lambda 表达式所在函数的局部变量,必须捕获该变量,即将此变量写在捕获列表中。捕获方式分为:引用捕获方式 [&]、值捕获方式 [=]。\nreturn type、parameter list、function body:分别表示返回值类型、参数列表、函数体,和普通函数一样。\n\n举例:lambda 表达式常搭配排序算法使用。\n#include <iostream>\n#include <vector>\n#include <algorithm>\nusing namespace std;\n\nint main()\n{\n vector<int> arr = {3, 4, 76, 12, 54, 90, 34};\n sort(arr.begin(), arr.end(), [](int a, int b) { return a > b; }); // 降序排序\n for (auto a : arr)\n {\n cout << a << " ";\n }\n return 0;\n}\n/*\n运行结果:90 76 54 34 12 4 3\n*/\n\nexplicit 的作用(如何避免编译器进行隐式类型转换)作用:用来声明类构造函数是显示调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换。只可用于修饰单参构造函数,因为无参构造函数和多参构造函数本身就是显示调用的,再加上 explicit 关键字也没有什么意义。\n隐式转换:\n#include <iostream>\n#include <cstring>\nusing namespace std;\n\nclass A\n{\npublic:\n int var;\n A(int tmp)\n {\n var = tmp;\n }\n};\nint main()\n{\n A ex = 10; // 发生了隐式转换\n return 0;\n}\n\n上述代码中,A ex = 10; 在编译时,进行了隐式转换,将 10 转换成 A 类型的对象,然后将该对象赋值给 ex,等同于如下操作:\n为了避免隐式转换,可用 explicit 关键字进行声明:\n#include <iostream>\n#include <cstring>\nusing namespace std;\n\nclass A\n{\npublic:\n int var;\n explicit A(int tmp)\n {\n var = tmp;\n cout << var << endl;\n }\n};\nint main()\n{\n A ex(100);\n A ex1 = 10; // error: conversion from 'int' to non-scalar type 'A' requested\n return 0;\n}\n\nC 和 C++ static 的区别在 C 语言中,使用 static 可以定义局部静态变量、外部静态变量、静态函数在 C++ 中,使用 static 可以定义局部静态变量、外部静态变量、静态函数、静态成员变量和静态成员函数。因为 C++中有类的概念,静态成员变量、静态成员函数都是与类有关的概念。\nstatic 的作用作用:static 定义静态变量,静态函数。\n保持变量内容持久:static 作用于局部变量,改变了局部变量的生存周期,使得该变量存在于定义后直到程序运行结束的这段时间。\n#include <iostream>\nusing namespace std;\n\nint fun(){\n static int var = 1; // var 只在第一次进入这个函数的时初始化\n var += 1;\n return var;\n}\n \nint main()\n{\n for(int i = 0; i < 10; ++i)\n \tcout << fun() << " "; // 2 3 4 5 6 7 8 9 10 11\n return 0;\n}\n\n隐藏:static作用于全局变量和函数,改变了全局变量和函数的作用域,使得全局变量和函数只能在定义它的文件中使用,在源文件中不具有全局可见性。(注:普通全局变量和函数具有全局可见性,即其他的源文件也可以使用。)\nstatic 作用于类的成员变量和类的成员函数,使得类变量或者类成员函数和类有关,也就是说可以不定义类的对象就可以通过类访问这些静态成员。注意:类的静态成员函数中只能访问静态成员变量或者静态成员函数,不能将静态成员函数定义成虚函数。\n#include<iostream>\nusing namespace std;\n\nclass A\n{\nprivate:\n int var;\n static int s_var; // 静态成员变量\npublic:\n void show()\n {\n cout << s_var++ << endl;\n }\n static void s_show()\n {\n cout << s_var << endl;\n\t\t// cout << var << endl; // error: invalid use of member 'A::a' in static member function. 静态成员函数不能调用非静态成员变量。无法使用 this.var\n // show(); // error: cannot call member function 'void A::show()' without object. 静态成员函数不能调用非静态成员函数。无法使用 this.show()\n }\n};\nint A::s_var = 1; // 静态成员变量在类外进行初始化赋值,默认初始化为 0\n\nint main()\n{\n \n // cout << A::sa << endl; // error: 'int A::sa' is private within this context\n A ex;\n ex.show();\n A::s_show();\n}\n\nstatic 在类中使用的注意事项(定义、初始化和使用)static 静态成员变量:\n\n静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现static关键字和private、public、protected 访问规则。\n静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。\n静态成员变量可以作为成员函数的参数,而普通成员变量不可以。\n\n#include <iostream>\nusing namespace std;\n\nclass A\n{\npublic:\n static int s_var;\n int var;\n void fun1(int i = s_var); // 正确,静态成员变量可以作为成员函数的参数\n void fun2(int i = var); // error: invalid use of non-static data member 'A::var'\n};\nint main()\n{\n return 0;\n}\n\n静态数据成员的类型可以是所属类的类型,而普通数据成员的类型只能是该类类型的指针或引用。\n#include <iostream>\nusing namespace std;\n\nclass A\n{\npublic:\n static A s_var; // 正确,静态数据成员\n A var; // error: field 'var' has incomplete type 'A'\n A *p; // 正确,指针\n A &var1; // 正确,引用\n};\n\nint main()\n{\n return 0;\n}\n\nstatic 静态成员函数:\n\n静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。静态成员函数做为类作用域的全局函数。\n静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数。\n\nstatic 全局变量和普通全局变量的异同相同点:\n\n存储方式:普通全局变量和 static 全局变量都是静态存储方式。\n\n不同点:\n\n作用域:普通全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,普通全局变量在各个源文件中都是有效的;静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。\n初始化:静态全局变量只初始化一次,防止在其他文件中使用。\n\nconst 作用及用法作用:\n\nconst 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。\nconst 修饰函数参数,使得传递过来的函数参数的值不能改变。\nconst 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。\n\n在类中的用法:\nconst 成员变量:\n\nconst 成员变量只能在类内声明、定义,在构造函数初始化列表中初始化。\nconst 成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的 const 成员变量的值是不同的。因此不能在类的声明中初始化 const 成员变量,类的对象还没有创建,编译器不知道他的值。\n\nconst 成员函数:\n\n不能修改成员变量的值,除非有 mutable 修饰;只能访问成员变量。\n不能调用非常量成员函数,以防修改成员变量的值。\n\ndefine 和 const 的区别区别:\n\n编译阶段:define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值。\n安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const 定义的常量是有类型的,是要进行判断的,可以避免一些低级的错误。\n内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的空间;const 定义的常量占用静态存储区的空间,程序运行过程中只有一份。\n调试:define 定义的宏常量不能调试,因为在预编译阶段就已经进行替换了;cons定义的常量可以进行调试。\n\nconst 的优点:\n\n有数据类型,在定义式可进行安全性检查。\n可调式。\n占用较少的空间。\n\ndefine 和 typedef 的区别\n原理:#define 作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字,在编译时处理,有类型检查功能,用来给一个已经存在的类型一个别名,但不能在一个函数定义里面使用 typedef 。\n功能:typedef 用来定义类型的别名,方便使用。#define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。\n作用域:#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。\n指针的操作:typedef 和 #define 在处理指针时不完全一样\n\n#include <iostream>\n#define INTPTR1 int *\ntypedef int * INTPTR2;\n\nusing namespace std;\n\nint main()\n{\n INTPTR1 p1, p2; // p1: int *; p2: int\n INTPTR2 p3, p4; // p3: int *; p4: int *\n\n int var = 1;\n const INTPTR1 p5 = &var; // 相当于 const int * p5; 常量指针,即不可以通过 p5 去修改 p5 指向的内容,但是 p5 可以指向其他内容。\n const INTPTR2 p6 = &var; // 相当于 int * const p6; 指针常量,不可使 p6 再指向其他内容。\n \n return 0;\n}\n\n用宏实现比较大小,以及两个数中的最小值#include <iostream>\n#define MAX(X, Y) ((X)>(Y)?(X):(Y))\n#define MIN(X, Y) ((X)<(Y)?(X):(Y))\nusing namespace std;\n\nint main ()\n{\n int var1 = 10, var2 = 100;\n cout << MAX(var1, var2) << endl;\n cout << MIN(var1, var2) << endl;\n return 0;\n}\n/*\n程序运行结果:\n100\n10\n*/\n\n4.12 inline 作用及使用方法作用:inline 是一个关键字,可以用于定义内联函数。内联函数,像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是直接在调用点处展开,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。\n使用方法:\n类内定义成员函数默认是内联函数在类内定义成员函数,可以不用在函数头部加 inline 关键字,因为编译器会自动将类内定义的函数(构造函数、析构函数、普通成员函数等)声明为内联函数,代码如下:\n#include <iostream>\nusing namespace std;\n\nclass A{\npublic:\n int var;\n A(int tmp){ \n var = tmp;\n } \n void fun(){ \n cout << var << endl;\n }\n};\n\nint main()\n{ \n return 0;\n}\n\n类外定义成员函数,若想定义为内联函数,需用关键字声明当在类内声明函数,在类外定义函数时,如果想将该函数定义为内联函数,则可以在类内声明时不加 inline 关键字,而在类外定义函数时加上 inline 关键字。\n#include <iostream>\nusing namespace std;\n\nclass A{\npublic:\n int var;\n A(int tmp){ \n var = tmp;\n } \n void fun();\n};\n\ninline void A::fun(){\n cout << var << endl;\n}\n\nint main()\n{ \n return 0;\n}\n\n另外,可以在声明函数和定义函数的同时加上 inline;也可以只在函数声明时加 inline,而定义函数时不加 inline。只要确保在调用该函数之前把 inline 的信息告知编译器即可。\ninline 函数工作原理\n内联函数不是在调用时发生控制转移关系,而是在编译阶段将函数体嵌入到每一个调用该函数的语句块中,编译器会将程序中出现内联函数的调用表达式用内联函数的函数体来替换。\n普通函数是将程序执行转移到被调用函数所存放的内存地址,当函数执行完后,返回到执行此函数前的地方。转移操作需要保护现场,被调函数执行完后,再恢复现场,该过程需要较大的资源开销。\n\n宏定义(define)和内联函数(inline)的区别\n内联函数是在编译时展开,而宏在编译预处理时展开;在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。\n内联函数是真正的函数,和普通函数调用的方法一样,在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销。而宏定义编写较为复杂,常需要增加一些括号来避免歧义。\n宏定义只进行文本替换,不会对参数的类型、语句能否正常编译等进行检查。而内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。\n\n#include <iostream>\n\n#define MAX(a, b) ((a) > (b) ? (a) : (b))\n\nusing namespace std;\n\ninline int fun_max(int a, int b)\n{\n return a > b ? a : b;\n}\n\nint main()\n{\n int var = 1;\n cout << MAX(var, 5) << endl; \n cout << fun_max(var, 0) << endl; \n return 0;\n}\n/*\n程序运行结果:\n5\n1\n\n*/\n\nnew 的作用?new 是 C++ 中的关键字,用来动态分配内存空间,实现方式如下:\nint *p = new int[5]; \n\nnew 和 malloc 如何判断是否申请到内存?\nmalloc :成功申请到内存,返回指向该内存的指针;分配失败,返回 NULL 指针。\nnew :内存分配成功,返回该对象类型的指针;分配失败,抛出 bac_alloc 异常。\n\ndelete 实现原理?delete 和 delete[] 的区别?delete 的实现原理:\n\n首先执行该对象所属类的析构函数;\n进而通过调用 operator delete 的标准库函数来释放所占的内存空间。\n\ndelete 和 delete [] 的区别:\n\ndelete 用来释放单个对象所占的空间,只会调用一次析构函数;\ndelete [] 用来释放数组空间,会对数组中的每个成员都调用一次析构函数。\n\nnew 和 malloc 的区别,delete 和 free 的区别在使用的时候 new、delete 搭配使用,malloc、free 搭配使用。\n\nmalloc、free 是库函数,而new、delete 是关键字。\nnew 申请空间时,无需指定分配空间的大小,编译器会根据类型自行计算;malloc 在申请空间时,需要确定所申请空间的大小。\nnew 申请空间时,返回的类型是对象的指针类型,无需强制类型转换,是类型安全的操作符;malloc 申请空间时,返回的是 void* 类型,需要进行强制类型的转换,转换为对象类型的指针。\nnew 分配失败时,会抛出 bad_alloc 异常,malloc 分配失败时返回空指针。\n对于自定义的类型,new 首先调用 operator new() 函数申请空间(底层通过 malloc 实现),然后调用构造函数进行初始化,最后返回自定义类型的指针;delete 首先调用析构函数,然后调用 operator delete() 释放空间(底层通过 free 实现)。malloc、free 无法进行自定义类型的对象的构造和析构。\nnew 操作符从自由存储区上为对象动态分配内存,而 malloc 函数从堆上动态分配内存。(自由存储区不等于堆)\n\nmalloc 的原理?malloc 的底层实现?malloc 的原理:\n\n当开辟的空间小于 128K 时,调用 brk() 函数,通过移动 _enddata 来实现;\n当开辟空间大于 128K 时,调用 mmap() 函数,通过在虚拟地址空间中开辟一块内存空间来实现。\n\nmalloc 的底层实现:\n\nbrk() 函数实现原理:向高地址的方向移动指向数据段的高地址的指针 _enddata。\nmmap 内存映射原理:\n进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域;\n调用内核空间的系统调用函数 mmap(),实现文件物理地址和进程虚拟地址的一一映射关系;\n进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝。\n\n\n\nC 和 C++ struct 的区别?\n在 C 语言中 struct 是用户自定义数据类型;在 C++ 中 struct 是抽象数据类型,支持成员函数的定义。\nC 语言中 struct 没有访问权限的设置,是一些变量的集合体,不能定义成员函数;C++ 中 struct 可以和类一样,有访问权限,并可以定义成员函数。\nC 语言中 struct 定义的自定义数据类型,在定义该类型的变量时,需要加上 struct 关键字,例如:struct A var;,定义 A 类型的变量;而 C++ 中,不用加该关键字,例如:A var;\n\n为什么有了 class 还保留 struct?C++ 是在 C 语言的基础上发展起来的,为了与 C 语言兼容,C++ 中保留了 struct。\nstruct 和 union 的区别说明:union 是联合体,struct 是结构体。\n区别:\n\n联合体和结构体都是由若干个数据类型不同的数据成员组成。使用时,联合体只有一个有效的成员;而结构体所有的成员都有效。\n对联合体的不同成员赋值,将会对覆盖其他成员的值,而对于结构体的对不同成员赋值时,相互不影响。\n联合体的大小为其内部所有变量的最大值,按照最大类型的倍数进行分配大小;结构体分配内存的大小遵循内存对齐原则。\n\n#include <iostream>\nusing namespace std;\n\ntypedef union\n{\n char c[10];\n char cc1; // char 1 字节,按该类型的倍数分配大小\n} u11;\n\ntypedef union\n{\n char c[10];\n int i; // int 4 字节,按该类型的倍数分配大小\n} u22;\n\ntypedef union\n{\n char c[10];\n double d; // double 8 字节,按该类型的倍数分配大小\n} u33;\n\ntypedef struct s1\n{\n char c; // 1 字节\n double d; // 1(char)+ 7(内存对齐)+ 8(double)= 16 字节\n} s11;\n\ntypedef struct s2\n{\n char c; // 1 字节\n char cc; // 1(char)+ 1(char)= 2 字节\n double d; // 2 + 6(内存对齐)+ 8(double)= 16 字节\n} s22;\n\ntypedef struct s3\n{\n char c; // 1 字节\n double d; // 1(char)+ 7(内存对齐)+ 8(double)= 16 字节\n char cc; // 16 + 1(char)+ 7(内存对齐)= 24 字节\n} s33;\n\nint main()\n{\n cout << sizeof(u11) << endl; // 10\n cout << sizeof(u22) << endl; // 12\n cout << sizeof(u33) << endl; // 16\n cout << sizeof(s11) << endl; // 16\n cout << sizeof(s22) << endl; // 16\n cout << sizeof(s33) << endl; // 24\n\n cout << sizeof(int) << endl; // 4\n cout << sizeof(double) << endl; // 8\n return 0;\n}\n\n4.23 class 和 struct 的异同\n\nstruct 和 class 都可以自定义数据类型,也支持继承操作。\nstruct 中默认的访问级别是 public,默认的继承级别也是 public;class 中默认的访问级别是 private,默认的继承级别也是 private。\n当 class 继承 struct 或者 struct 继承 class 时,默认的继承级别取决于 class 或 struct 本身,class(private 继承),struct(public 继承),即取决于派生类的默认继承级别。\n\nstruct A{};\nclass B : A{}; // private 继承 \nstruct C : B{}; // public 继承\n\n举例:\n#include<iostream>\n\nusing namespace std;\n\nclass A{\npublic:\n void funA(){\n cout << "class A" << endl;\n }\n};\n\nstruct B: A{ // 由于 B 是 struct,A 的默认继承级别为 public\npublic:\n void funB(){\n cout << "class B" << endl;\n }\n};\n\nclass C: B{ // 由于 C 是 class,B 的默认继承级别为 private,所以无法访问基类 B 中的 printB 函数\n\n};\n\nint main(){\n A ex1;\n ex1.funA(); // class A\n\n B ex2;\n ex2.funA(); // class A\n ex2.funB(); // class B\n\n C ex3;\n ex3.funB(); // error: 'B' is not an accessible base of 'C'.\n return 0;\n}\n\nclass 可以用于定义模板参数,struct 不能用于定义模板参数。\nvolatile 的作用?是否具有原子性,对编译器有什么影响?volatile 的作用:当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 violatile,告知编译器不应对这样的对象进行优化。\nvolatile不具有原子性。\nvolatile 对编译器的影响:使用该关键字后,编译器不会对相应的对象进行优化,即不会将变量从内存缓存到寄存器中,防止多个线程有可能使用内存中的变量,有可能使用寄存器中的变量,从而导致程序错误。\n什么情况下一定要用 volatile, 能否和 const 一起使用?使用 volatile 关键字的场景:\n\n当多个线程都会用到某一变量,并且该变量的值有可能发生改变时,需要用 volatile 关键字对该变量进行修饰;\n中断服务程序中访问的变量或并行设备的硬件寄存器的变量,最好用 volatile 关键字修饰。\n\nvolatile 关键字和 const 关键字可以同时使用,某种类型可以既是 volatile 又是 const ,同时具有二者的属性。\n返回函数中静态变量的地址会发生什么?#include <iostream>\nusing namespace std;\n\nint * fun(int tmp){\n static int var = 10;\n var *= tmp;\n return &var;\n}\n\nint main() {\n cout << *fun(5) << endl;\n return 0;\n}\n\n/*\n运行结果:\n50\n*/\n\n说明:上述代码中在函数 fun 中定义了静态局部变量 var,使得离开该函数的作用域后,该变量不会销毁,返回到主函数中,该变量依然存在,从而使程序得到正确的运行结果。但是,该静态局部变量直到程序运行结束后才销毁,浪费内存空间。\nextern C 的作用?当 C++ 程序 需要调用 C 语言编写的函数,C++ 使用链接指示,即 extern “C” 指出任意非 C++ 函数所用的语言。举例:\n// 可能出现在 C++ 头文件<cstring>中的链接指示\nextern "C"{\n int strcmp(const char*, const char*);\n}\n\n4.28 sizeof(1==1) 在 C 和 C++ 中分别是什么结果?C 语言代码:\n#include<stdio.h>\n\nvoid main(){\n printf("%d\\n", sizeof(1==1));\n}\n\n/*\n运行结果:\n4\n*/\n\nC++ 代码:\n#include <iostream>\nusing namespace std;\n\nint main() {\n cout << sizeof(1==1) << endl;\n return 0;\n}\n\n/*\n1\n*/\n\nmemcpy 函数的底层原理?void *memcpy(void *dst, const void *src, size_t size)\n{\n char *psrc;\n char *pdst;\n\n if (NULL == dst || NULL == src)\n {\n return NULL;\n }\n\n if ((src < dst) && (char *)src + size > (char *)dst) // 出现地址重叠的情况,自后向前拷贝\n {\n psrc = (char *)src + size - 1;\n pdst = (char *)dst + size - 1;\n while (size--)\n {\n *pdst-- = *psrc--;\n }\n }\n else\n {\n psrc = (char *)src;\n pdst = (char *)dst;\n while (size--)\n {\n *pdst++ = *psrc++;\n }\n }\n\n return dst;\n}\n\nstrcpy 函数有什么缺陷?strcpy 函数的缺陷:strcpy 函数不检查目的缓冲区的大小边界,而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间,同时加上字符串终止符,会导致其他变量被覆盖。\n#include <iostream>\n#include <cstring>\nusing namespace std;\n\nint main()\n{\n int var = 0x11112222;\n char arr[10];\n cout << "Address : var " << &var << endl;\n cout << "Address : arr " << &arr << endl;\n strcpy(arr, "hello world!");\n cout << "var:" << hex << var << endl; // 将变量 var 以 16 进制输出\n cout << "arr:" << arr << endl;\n return 0;\n}\n\n/*\nAddress : var 0x23fe4c\nAddress : arr 0x23fe42\nvar:11002164\narr:hello world!\n*/\n\n说明:从上述代码中可以看出,变量 var 的后六位被字符串 “hello world!” 的 “d!\\0” 这三个字符改变,这三个字符对应的 ascii 码的十六进制为:\\0(0x00),!(0x21),d(0x64)。原因:变量 arr 只分配的 10 个内存空间,通过上述程序中的地址可以看出 arr 和 var 在内存中是连续存放的,但是在调用 strcpy 函数进行拷贝时,源字符串 “hello world!” 所占的内存空间为 13,因此在拷贝的过程中会占用 var 的内存空间,导致 var的后六位被覆盖。\nauto 类型推导的原理auto 类型推导的原理:编译器根据初始值来推算变量的类型,要求用 auto 定义变量时必须有初始值。编译器推断出来的 auto 类型有时和初始值类型并不完全一样,编译器会适当改变结果类型使其更符合初始化规则。\n类相关什么是虚函数?什么是纯虚函数?虚函数:被 virtual 关键字修饰的成员函数,就是虚函数。\n#include <iostream>\nusing namespace std;\n\nclass A\n{\npublic:\n virtual void v_fun() // 虚函数\n {\n cout << "A::v_fun()" << endl;\n }\n};\nclass B : public A\n{\npublic:\n void v_fun()\n {\n cout << "B::v_fun()" << endl;\n }\n};\nint main()\n{\n A *p = new B();\n p->v_fun(); // B::v_fun()\n return 0;\n}\n\n纯虚函数:\n\n纯虚函数在类中声明时,加上 =0;\n含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法;\n继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。\n\n说明:\n\n抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;\n可以声明抽象类指针,可以声明抽象类的引用;\n子类必须继承父类的纯虚函数,并全部实现后,才能创建子类的对象。\n\n虚函数和纯虚函数的区别?\n虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类。(含有纯虚函数的类称为抽象基类)\n使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用;\n定义形式不同:虚函数在定义时在普通函数的基础上加上 virtual 关键字,纯虚函数定义时除了加上virtual 关键字还需要加上 =0;\n虚函数必须实现,否则编译器会报错;\n对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数,虚函数和纯虚函数都可以在派生类中重写;\n析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。\n\n虚函数的实现机制实现机制:虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数\n虚函数表相关知识点:\n\n虚函数表存放的内容:类的虚函数的地址。\n虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。\n虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。\n\n注:虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象都有自己的虚表指针,来指向类的虚函数表。\n实例:\n无虚函数覆盖的情况:\n#include <iostream>\nusing namespace std;\n\nclass Base\n{\npublic:\n virtual void B_fun1() { cout << "Base::B_fun1()" << endl; }\n virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }\n virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }\n};\n\nclass Derive : public Base\n{\npublic:\n virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }\n virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }\n virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }\n};\nint main()\n{\n Base *p = new Derive();\n p->B_fun1(); // Base::B_fun1()\n return 0;\n}\n\n基类和派生类的继承关系:\n\n基类的虚函数表:\n\n派生类的虚函数表:\n\n主函数中基类的指针 p 指向了派生类的对象,当调用函数 B_fun1() 时,通过派生类的虚函数表找到该函数的地址,从而完成调用。\n单继承和多继承的虚函数表结构编译器处理虚函数表:\n\n编译器将虚函数表的指针放在类的实例对象的内存空间中,该对象调用该类的虚函数时,通过指针找到虚函数表,根据虚函数表中存放的虚函数的地址找到对应的虚函数。\n如果派生类没有重新定义基类的虚函数 A,则派生类的虚函数表中保存的是基类的虚函数 A 的地址,也就是说基类和派生类的虚函数 A 的地址是一样的。\n如果派生类重写了基类的某个虚函数 B,则派生的虚函数表中保存的是重写后的虚函数 B 的地址,也就是说虚函数 B 有两个版本,分别存放在基类和派生类的虚函数表中。\n如果派生类重新定义了新的虚函数 C,派生类的虚函数表保存新的虚函数 C 的地址。\n\n单继承无虚函数覆盖的情况:\n#include <iostream>\nusing namespace std;\n\nclass Base\n{\npublic:\n virtual void B_fun1() { cout << "Base::B_fun1()" << endl; }\n virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }\n virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }\n};\n\nclass Derive : public Base\n{\npublic:\n virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }\n virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }\n virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }\n};\nint main()\n{\n Base *p = new Derive();\n p->B_fun1(); // Base::B_fun1()\n return 0;\n}\n\n基类和派生类的继承关系:\n\n基类的虚函数表:\n\n派生类的虚函数表:\n\n单继承有虚函数覆盖的情况:\n#include <iostream>\nusing namespace std;\n\nclass Base\n{\npublic:\n virtual void fun1() { cout << "Base::fun1()" << endl; }\n virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }\n virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }\n};\n\nclass Derive : public Base\n{\npublic:\n virtual void fun1() { cout << "Derive::fun1()" << endl; }\n virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }\n virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }\n};\nint main()\n{\n Base *p = new Derive();\n p->fun1(); // Derive::fun1()\n return 0;\n}\n\n派生类的虚函数表:\n\n多继承无虚函数覆盖的情况:\n#include <iostream>\nusing namespace std;\n\nclass Base1\n{\npublic:\n virtual void B1_fun1() { cout << "Base1::B1_fun1()" << endl; }\n virtual void B1_fun2() { cout << "Base1::B1_fun2()" << endl; }\n virtual void B1_fun3() { cout << "Base1::B1_fun3()" << endl; }\n};\nclass Base2\n{\npublic:\n virtual void B2_fun1() { cout << "Base2::B2_fun1()" << endl; }\n virtual void B2_fun2() { cout << "Base2::B2_fun2()" << endl; }\n virtual void B2_fun3() { cout << "Base2::B2_fun3()" << endl; }\n};\nclass Base3\n{\npublic:\n virtual void B3_fun1() { cout << "Base3::B3_fun1()" << endl; }\n virtual void B3_fun2() { cout << "Base3::B3_fun2()" << endl; }\n virtual void B3_fun3() { cout << "Base3::B3_fun3()" << endl; }\n};\n\nclass Derive : public Base1, public Base2, public Base3\n{\npublic:\n virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }\n virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }\n virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }\n};\n\nint main(){\n Base1 *p = new Derive();\n p->B1_fun1(); // Base1::B1_fun1()\n return 0;\n}\n\n基类和派生类的关系:\n\n派生类的虚函数表:(基类的顺序和声明的顺序一致)\n\n多继承有虚函数覆盖的情况:\n#include <iostream>\nusing namespace std;\n\nclass Base1\n{\npublic:\n virtual void fun1() { cout << "Base1::fun1()" << endl; }\n virtual void B1_fun2() { cout << "Base1::B1_fun2()" << endl; }\n virtual void B1_fun3() { cout << "Base1::B1_fun3()" << endl; }\n};\nclass Base2\n{\npublic:\n virtual void fun1() { cout << "Base2::fun1()" << endl; }\n virtual void B2_fun2() { cout << "Base2::B2_fun2()" << endl; }\n virtual void B2_fun3() { cout << "Base2::B2_fun3()" << endl; }\n};\nclass Base3\n{\npublic:\n virtual void fun1() { cout << "Base3::fun1()" << endl; }\n virtual void B3_fun2() { cout << "Base3::B3_fun2()" << endl; }\n virtual void B3_fun3() { cout << "Base3::B3_fun3()" << endl; }\n};\n\nclass Derive : public Base1, public Base2, public Base3\n{\npublic:\n virtual void fun1() { cout << "Derive::fun1()" << endl; }\n virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }\n virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }\n};\n\nint main(){\n Base1 *p1 = new Derive();\n Base2 *p2 = new Derive();\n Base3 *p3 = new Derive();\n p1->fun1(); // Derive::fun1()\n p2->fun1(); // Derive::fun1()\n p3->fun1(); // Derive::fun1()\n return 0;\n}\n\n基类和派生类的关系:\n\n派生类的虚函数表:\n\n语言特性相关左值和右值的区别?左值引用和右值引用的区别,如何将左值转换成右值?左值:指表达式结束后依然存在的持久对象。\n右值:表达式结束就不再存在的临时对象。\n左值和右值的区别:左值持久,右值短暂\n右值引用和左值引用的区别:\n\n左值引用不能绑定到要转换的表达式、字面常量或返回右值的表达式。右值引用恰好相反,可以绑定到这类表达式,但不能绑定到一个左值上。\n右值引用必须绑定到右值的引用,通过 && 获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。\n\nstd::move 可以将一个左值强制转化为右值,继而可以通过右值引用使用该值,以用于移动语义。\n#include <iostream>\nusing namespace std;\n\nvoid fun1(int& tmp) \n{ \n cout << "fun1(int& tmp):" << tmp << endl; \n} \n\nvoid fun2(int&& tmp) \n{ \n cout << "fun2(int&& tmp)" << tmp << endl; \n} \n\nint main() \n{ \n int var = 11; \n fun1(12); // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'\n fun1(var);\n fun2(1); \n}\n\nstd::move() 函数的实现原理std::move() 函数原型:\ntemplate <typename T>\ntypename remove_reference<T>::type&& move(T&& t)\n{\n\treturn static_cast<typename remove_reference<T>::type &&>(t);\n}\n\n说明:引用折叠原理\n\n右值传递给上述函数的形参 T&& 依然是右值,即 T&& && 相当于 T&&。\n左值传递给上述函数的形参 T&& 依然是左值,即 T&& & 相当于 T&。\n\n小结:通过引用折叠原理可以知道,move() 函数的形参既可以是左值也可以是右值。\nremove_reference 具体实现:\n//原始的,最通用的版本\ntemplate <typename T> struct remove_reference{\n typedef T type; //定义 T 的类型别名为 type\n};\n \n//部分版本特例化,将用于左值引用和右值引用\ntemplate <class T> struct remove_reference<T&> //左值引用\n{ typedef T type; }\n \ntemplate <class T> struct remove_reference<T&&> //右值引用\n{ typedef T type; } \n \n//举例如下,下列定义的a、b、c三个变量都是int类型\nint i;\nremove_refrence<decltype(42)>::type a; //使用原版本,\nremove_refrence<decltype(i)>::type b; //左值引用特例版本\nremove_refrence<decltype(std::move(i))>::type b; //右值引用特例版本 \n\n举例:\nint var = 10; \n\n转化过程:\n1. std::move(var) => std::move(int&& &) => 折叠后 std::move(int&)\n\n2. 此时:T 的类型为 int&,typename remove_reference<T>::type 为 int,这里使用 remove_reference 的左值引用的特例化版本\n\n3. 通过 static_cast 将 int& 强制转换为 int&&\n\n整个std::move被实例化如下\nstring&& move(int& t) \n{\n return static_cast<int&&>(t); \n}\n\n总结:std::move() 实现原理:\n\n利用引用折叠原理将右值经过 T&& 传递类型保持不变还是右值,而左值经过 T&& 变为普通的左值引用,以保证模板可以传递任意实参,且保持类型不变;\n然后通过 remove_refrence 移除引用,得到具体的类型 T;\n最后通过 static_cast<> 进行强制类型转换,返回 T&& 右值引用。\n\n什么是指针?指针的大小及用法?指针: 指向另外一种类型的复合类型。指针的大小: 在 64 位计算机中,指针占 8 个字节空间。\n#include<iostream>\n\nusing namespace std;\n\nint main(){\n int *p = nullptr;\n cout << sizeof(p) << endl; // 8\n\n char *p1 = nullptr;\n cout << sizeof(p1) << endl; // 8\n return 0;\n}\n\n指针的用法:\n指向普通对象的指针:\n#include <iostream>\n\nusing namespace std;\n\nclass A\n{\n};\n\nint main()\n{\n A *p = new A();\n return 0;\n}\n\n指向常量对象的指针:常量指针\n#include <iostream>\nusing namespace std;\n\nint main(void)\n{\n const int c_var = 10;\n const int * p = &c_var;\n cout << *p << endl;\n return 0;\n}\n\n指向函数的指针:函数指针\n#include <iostream>\nusing namespace std;\n\nint add(int a, int b){\n return a + b;\n}\n\nint main(void)\n{\n int (*fun_p)(int, int);\n fun_p = add;\n cout << fun_p(1, 6) << endl;\n return 0;\n}\n\n指向对象成员的指针,包括指向对象成员函数的指针和指向对象成员变量的指针。特别注意:定义指向成员函数的指针时,要标明指针所属的类。\n#include <iostream>\n\nusing namespace std;\n\nclass A\n{\npublic:\n int var1, var2; \n int add(){\n return var1 + var2;\n }\n};\n\nint main()\n{\n A ex;\n ex.var1 = 3;\n ex.var2 = 4;\n int *p = &ex.var1; // 指向对象成员变量的指针\n cout << *p << endl;\n\n int (A::*fun_p)();\n fun_p = A::add; // 指向对象成员函数的指针 fun_p\n cout << (ex.*fun_p)() << endl;\n return 0;\n}\n\nthis 指针:指向类的当前对象的指针常量。\n#include <iostream>\n#include <cstring>\nusing namespace std;\n\nclass A\n{\npublic:\n void set_name(string tmp)\n {\n this->name = tmp;\n }\n void set_age(int tmp)\n {\n this->age = age;\n }\n void set_sex(int tmp)\n {\n this->sex = tmp;\n }\n void show()\n {\n cout << "Name: " << this->name << endl;\n cout << "Age: " << this->age << endl;\n cout << "Sex: " << this->sex << endl;\n }\n\nprivate:\n string name;\n int age;\n int sex;\n};\n\nint main()\n{\n A *p = new A();\n p->set_name("Alice");\n p->set_age(16);\n p->set_sex(1);\n p->show();\n\n return 0;\n}\n\n什么是野指针和悬空指针?悬空指针: 若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间,此时,称该指针为“悬空指针”。\nvoid *p = malloc(size);\nfree(p); \n// 此时,p 指向的内存空间已释放, p 就是悬空指针。\n\n野指针:\n“野指针”是指不确定其指向的指针,未初始化的指针为“野指针”。\nvoid *p; \n// 此时 p 是“野指针”。\n\nC++ 11 nullptr 比 NULL 优势\nNULL:预处理变量,是一个宏,它的值是 0,定义在头文件 中,即 #define NULL 0。\nnullptr:C++ 11 中的关键字,是一种特殊类型的字面值,可以被转换成任意其他类型。\n\nnullptr 的优势:\n\n有类型,类型是 typdef decltype(nullptr) nullptr_t;,使用 nullptr 提高代码的健壮性。\n函数重载:因为 NULL 本质上是 0,在函数调用过程中,若出现函数重载并且传递的实参是 NULL,可能会出现,不知和哪一个函数匹配的情况;但是传递实参 nullptr 就不会出现这种情况。\n\n#include <iostream>\n#include <cstring>\nusing namespace std;\n\nvoid fun(char const *p)\n{\n cout << "fun(char const *p)" << endl;\n}\n\nvoid fun(int tmp)\n{\n cout << "fun(int tmp)" << endl;\n}\n\nint main()\n{\n fun(nullptr); // fun(char const *p)\n /*\n fun(NULL); // error: call of overloaded 'fun(NULL)' is ambiguous\n */\n return 0;\n}\n\n指针和引用的区别?\n指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。(是否可变)\n指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占内存空间。(是否占内存)\n指针可以为空,但是引用必须绑定对象。(是否可为空)\n指针可以有多级,但是引用只能一级。(是否能为多级)\n\n6.7 常量指针和指针常量的区别常量指针:常量指针本质上是个指针,只不过这个指针指向的对象是常量。特点:const 的位置在指针声明运算符 * 的左侧。只要 const 位于 * 的左侧,无论它在类型名的左边或右边,都表示指向常量的指针。(可以这样理解,* 左侧表示指针指向的对象,该对象为常量,那么该指针为常量指针。)\nconst int * p;\nint const * p;\n\n注意 1:指针指向的对象不能通过这个指针来修改,也就是说常量指针可以被赋值为变量的地址,之所以叫做常量指针,是限制了通过这个指针修改变量的值。\n#include <iostream>\nusing namespace std;\n\nint main()\n{\n const int c_var = 8;\n const int *p = &c_var; \n *p = 6; // error: assignment of read-only location '* p'\n return 0;\n}\n\n注意 2:虽然常量指针指向的对象不能变化,可是因为常量指针本身是一个变量,因此,可以被重新赋值。例如:\n#include <iostream>\nusing namespace std;\n\nint main()\n{\n const int c_var1 = 8;\n const int c_var2 = 8;\n const int *p = &c_var1; \n p = &c_var2;\n return 0;\n}\n\n指针常量:指针常量的本质上是个常量,只不过这个常量的值是一个指针。特点:const 位于指针声明操作符右侧,表明该对象本身是一个常量,* 左侧表示该指针指向的类型,即以 * 为分界线,其左侧表示指针指向的类型,右侧表示指针本身的性质。\nconst int var;\nint * const c_p = &var; \n\n注意 1:指针常量的值是指针,这个值因为是常量,所以指针本身不能改变。\n#include <iostream>\nusing namespace std;\n\nint main()\n{\n int var, var1;\n int * const c_p = &var;\n c_p = &var1; // error: assignment of read-only variable 'c_p'\n return 0;\n}\n\n注意 2:指针的内容可以改变。\n#include <iostream>\nusing namespace std;\n\nint main()\n{\n int var = 3;\n int * const c_p = &var;\n *c_p = 12; \n return 0;\n}\n\n函数指针和指针函数的区别指针函数:指针函数本质是一个函数,只不过该函数的返回值是一个指针。相对于普通函数而言,只是返回值是指针。\n#include <iostream>\nusing namespace std;\n\nstruct Type\n{\n int var1;\n int var2;\n};\n\nType * fun(int tmp1, int tmp2){\n Type * t = new Type();\n t->var1 = tmp1;\n t->var2 = tmp2;\n return t;\n}\n\nint main()\n{\n Type *p = fun(5, 6);\n return 0;\n}\n\n函数指针:函数指针本质是一个指针变量,只不过这个指针指向一个函数。函数指针即指向函数的指针。\n举例:\n#include <iostream>\nusing namespace std;\nint fun1(int tmp1, int tmp2)\n{\n return tmp1 * tmp2;\n}\nint fun2(int tmp1, int tmp2)\n{\n return tmp1 / tmp2;\n}\n\nint main()\n{\n int (*fun)(int x, int y); \n fun = fun1;\n cout << fun(15, 5) << endl; \n fun = fun2;\n cout << fun(15, 5) << endl; \n return 0;\n}\n/*\n运行结果:\n75\n3\n*/\n\n函数指针和指针函数的区别:\n\n本质不同 1.指针函数本质是一个函数,其返回值为指针。 2.函数指针本质是一个指针变量,其指向一个函数。\n定义形式不同 1.指针函数:int* fun(int tmp1, int tmp2); ,这里* 表示函数的返回值类型是指针类型。 2.函数指针:int (fun)(int tmp1, int tmp2);,这里 表示变量本身是指针类型。\n用法不同\n\n强制类型转换有哪几种?\nstatic_cast:用于数据的强制类型转换,强制将一种数据类型转换为另一种数据类型。 1.用于基本数据类型的转换。 2.用于类层次之间的基类和派生类之间 指针或者引用 的转换(不要求必须包含虚函数,但必须是有相互联系的类),进行上行转换(派生类的指针或引用转换成基类表示)是安全的;进行下行转换(基类的指针或引用转换成派生类表示)由于没有动态类型检查,所以是不安全的,最好用 dynamic_cast 进行下行转换。 3.可以将空指针转化成目标类型的空指针。 4.可以将任何类型的表达式转化成 void 类型。\nconst_cast:强制去掉常量属性,不能用于去掉变量的常量性,只能用于去除指针或引用的常量性,将常量指针转化为非常量指针或者将常量引用转化为非常量引用(注意:表达式的类型和要转化的类型是相同的)。\nreinterpret_cast:改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转化为指针或引用类型。\ndynamic_cast: 1.其他三种都是编译时完成的,动态类型转换是在程序运行时处理的,运行时会进行类型检查。 2.只能用于带有虚函数的基类或派生类的指针或者引用对象的转换,转换成功返回指向类型的指针或引用,转换失败返回 NULL;不能用于基本数据类型的转换。 3.在向上进行转换时,即派生类类的指针转换成基类类的指针和 static_cast 效果是一样的,(注意:这里只是改变了指针的类型,指针指向的对象的类型并未发生改变)。\n\n#include <iostream>\n#include <cstring>\n\nusing namespace std;\n\nclass Base\n{\n};\n\nclass Derive : public Base\n{\n};\n\nint main()\n{\n Base *p1 = new Derive();\n Derive *p2 = new Derive();\n\n //向上类型转换\n p1 = dynamic_cast<Base *>(p2);\n if (p1 == NULL)\n {\n cout << "NULL" << endl;\n }\n else\n {\n cout << "NOT NULL" << endl; //输出\n }\n\n return 0;\n}\n\n4. 在下行转换时,基类的指针类型转化为派生类类的指针类型,只有当要转换的指针指向的对象类型和转化以后的对象类型相同时,才会转化成功。\n\n#include <iostream>\n#include <cstring>\n\nusing namespace std;\n\nclass Base\n{\npublic:\n virtual void fun()\n {\n cout << "Base::fun()" << endl;\n }\n};\n\nclass Derive : public Base\n{\npublic:\n virtual void fun()\n {\n cout << "Derive::fun()" << endl;\n }\n};\n\nint main()\n{\n Base *p1 = new Derive();\n Base *p2 = new Base();\n Derive *p3 = new Derive();\n\n //转换成功\n p3 = dynamic_cast<Derive *>(p1);\n if (p3 == NULL)\n {\n cout << "NULL" << endl;\n }\n else\n {\n cout << "NOT NULL" << endl; // 输出\n }\n\n //转换失败\n p3 = dynamic_cast<Derive *>(p2);\n if (p3 == NULL)\n {\n cout << "NULL" << endl; // 输出\n }\n else\n {\n cout << "NOT NULL" << endl;\n }\n\n return 0;\n}\n\n\n如何判断结构体是否相等?能否用 memcmp 函数判断结构体相等?需要重载操作符 == 判断两个结构体是否相等,不能用函数 memcmp 来判断两个结构体是否相等,因为 memcmp 函数是逐个字节进行比较的,而结构体存在内存空间中保存时存在字节对齐,字节对齐时补的字节内容是随机的,会产生垃圾值,所以无法比较。\n利用运算符重载来实现结构体对象的比较:\n#include <iostream>\n\nusing namespace std;\n\nstruct A\n{\n char c;\n int val;\n A(char c_tmp, int tmp) : c(c_tmp), val(tmp) {}\n\n friend bool operator==(const A &tmp1, const A &tmp2); // 友元运算符重载函数\n};\n\nbool operator==(const A &tmp1, const A &tmp2)\n{\n return (tmp1.c == tmp2.c && tmp1.val == tmp2.val);\n}\n\nint main()\n{\n A ex1('a', 90), ex2('b', 80);\n if (ex1 == ex2)\n cout << "ex1 == ex2" << endl;\n else\n cout << "ex1 != ex2" << endl; // 输出\n return 0;\n}\n\n参数传递时,值传递、引用传递、指针传递的区别?参数传递的三种方式:\n\n值传递:形参是实参的拷贝,函数对形参的所有操作不会影响实参。\n指针传递:本质上是值传递,只不过拷贝的是指针的值,拷贝之后,实参和形参是不同的指针,通过指针可以间接的访问指针所指向的对象,从而可以修改它所指对象的值。\n引用传递:当形参是引用类型时,我们说它对应的实参被引用传递。\n\n#include <iostream>\nusing namespace std;\n\nvoid fun1(int tmp){ // 值传递\n cout << &tmp << endl;\n}\n\nvoid fun2(int * tmp){ // 指针传递\n cout << tmp << endl;\n}\n\nvoid fun3(int &tmp){ // 引用传递\n cout << &tmp << endl;\n}\n\nint main()\n{\n int var = 5;\n cout << "var 在主函数中的地址:" << &var << endl;\n\n cout << "var 值传递时的地址:";\n fun1(var);\n\n cout << "var 指针传递时的地址:";\n fun2(&var);\n\n cout << "var 引用传递时的地址:";\n fun3(var);\n return 0;\n}\n\n/*\n运行结果:\nvar 在主函数中的地址:0x23fe4c\nvar 值传递时的地址:0x23fe20\nvar 指针传递时的地址:0x23fe4c\nvar 引用传递时的地址:0x23fe4c\n*/\n\n说明:从上述代码的运行结果可以看出,只有在值传递时,形参和实参的地址不一样,在函数体内操作的不是变量本身。引用传递和指针传递,在函数体内操作的是变量本身。\n6.12 什么是模板?如何实现?模板:创建类或者函数的蓝图或者公式,分为函数模板和类模板。实现方式:模板定义以关键字 template 开始,后跟一个模板参数列表。\n\n模板参数列表不能为空;\n模板类型参数前必须使用关键字 class 或者 typename,在模板参数列表中这两个关键字含义相同,可互换使用。\n\ntemplate <typename T, typename U, ...>\n\n函数模板:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。\n\n对于函数模板而言,模板类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。\n函数模板实例化:当调用一个模板时,编译器用函数实参来推断模板实参,从而使用实参的类型来确定绑定到模板参数的类型。\n\n#include<iostream>\n\nusing namespace std;\n\ntemplate <typename T>\nT add_fun(const T & tmp1, const T & tmp2){\n return tmp1 + tmp2;\n}\n\nint main(){\n int var1, var2;\n cin >> var1 >> var2;\n cout << add_fun(var1, var2);\n\n double var3, var4;\n cin >> var3 >> var4;\n cout << add_fun(var3, var4);\n return 0;\n}\n\n类模板:类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型。\n#include <iostream>\n\nusing namespace std;\n\ntemplate <typename T>\nclass Complex\n{\npublic:\n //构造函数\n Complex(T a, T b)\n {\n this->a = a;\n this->b = b;\n }\n\n //运算符重载\n Complex<T> operator+(Complex &c)\n {\n Complex<T> tmp(this->a + c.a, this->b + c.b);\n cout << tmp.a << " " << tmp.b << endl;\n return tmp;\n }\n\nprivate:\n T a;\n T b;\n};\n\nint main()\n{\n Complex<int> a(10, 20);\n Complex<int> b(20, 30);\n Complex<int> c = a + b;\n\n return 0;\n}\n\n函数模板和类模板的区别?\n实例化方式不同:函数模板实例化由编译程序在处理函数调用时自动完成,类模板实例化需要在程序中显式指定。\n实例化的结果不同:函数模板实例化后是一个函数,类模板实例化后是一个类。\n默认参数:类模板在模板参数列表中可以有默认参数。\n特化:函数模板只能全特化;而类模板可以全特化,也可以偏特化。\n调用方式不同:函数模板可以隐式调用,也可以显式调用;类模板只能显式调用。\n\n函数模板调用方式举例:\n#include<iostream>\n\nusing namespace std;\n\ntemplate <typename T>\nT add_fun(const T & tmp1, const T & tmp2){\n return tmp1 + tmp2;\n}\n\nint main(){\n int var1, var2;\n cin >> var1 >> var2;\n cout << add_fun<int>(var1, var2); // 显式调用\n\n double var3, var4;\n cin >> var3 >> var4;\n cout << add_fun(var3, var4); // 隐式调用\n return 0;\n}\n\n6.14 什么是可变参数模板?可变参数模板:接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包,包括模板参数包和函数参数包。\n\n模板参数包:表示零个或多个模板参数;\n函数参数包:表示零个或多个函数参数。\n\n用省略号来指出一个模板参数或函数参数表示一个包,在模板参数列表中,class… 或 typename… 指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。当需要知道包中有多少元素时,可以使用 sizeof… 运算符。\ntemplate <typename T, typename... Args> // Args 是模板参数包\nvoid foo(const T &t, const Args&... rest); // 可变参数模板,rest 是函数参数包\n\n#include <iostream>\n\nusing namespace std;\n\ntemplate <typename T>\nvoid print_fun(const T &t)\n{\n cout << t << endl; // 最后一个元素\n}\n\ntemplate <typename T, typename... Args>\nvoid print_fun(const T &t, const Args &...args)\n{\n cout << t << " ";\n print_fun(args...);\n}\n\nint main()\n{\n print_fun("Hello", "wolrd", "!");\n return 0;\n}\n/*运行结果:\nHello wolrd !\n\n*/\n\n说明:可变参数函数通常是递归的,第一个版本的 print_fun 负责终止递归并打印初始调用中的最后一个实参。第二个版本的 print_fun 是可变参数版本,打印绑定到 t 的实参,并用来调用自身来打印函数参数包中的剩余值。\n什么是模板特化?为什么特化?模板特化的原因:模板并非对任何模板实参都合适、都能实例化,某些情况下,通用模板的定义对特定类型不合适,可能会编译失败,或者得不到正确的结果。因此,当不希望使用模板版本时,可以定义类或者函数模板的一个特例化版本。\n模板特化:模板参数在某种特定类型下的具体实现。分为函数模板特化和类模板特化\n\n函数模板特化:将函数模板中的全部类型进行特例化,称为函数模板特化。\n类模板特化:将类模板中的部分或全部类型进行特例化,称为类模板特化。\n\n特化分为全特化和偏特化:\n\n全特化:模板中的模板参数全部特例化。\n偏特化:模板中的模板参数只确定了一部分,剩余部分需要在编译器编译时确定。\n\n说明:要区分下函数重载与函数模板特化定义函数模板的特化版本,本质上是接管了编译器的工作,为原函数模板定义了一个特殊实例,而不是函数重载,函数模板特化并不影响函数匹配。\n#include <iostream>\n#include <cstring>\n\nusing namespace std;\n//函数模板\ntemplate <class T>\nbool compare(T t1, T t2)\n{\n cout << "通用版本:";\n return t1 == t2;\n}\n\ntemplate <> //函数模板特化\nbool compare(char *t1, char *t2)\n{\n cout << "特化版本:";\n return strcmp(t1, t2) == 0;\n}\n\nint main(int argc, char *argv[])\n{\n char arr1[] = "hello";\n char arr2[] = "abc";\n cout << compare(123, 123) << endl;\n cout << compare(arr1, arr2) << endl;\n\n return 0;\n}\n/*\n运行结果:\n通用版本:1\n特化版本:0\n*/\n\ninclude “ “ 和 <> 的区别include<文件名> 和 #include”文件名” 的区别:\n\n查找文件的位置:include<文件名>在标准库头文件所在的目录中查找,如果没有,再到当前源文件所在目录下查找;#include”文件名” 在当前源文件所在目录中进行查找,如果没有;再到系统目录中查找。\n使用习惯:对于标准库中的头文件常用 include<文件名>,对于自己定义的头文件,常用 #include”文件名”\n\n泛型编程如何实现?泛型编程实现的基础:模板。模板是创建类或者函数的蓝图或者说公式,当时用一个 vector 这样的泛型,或者 find 这样的泛型函数时,编译时会转化为特定的类或者函数。\n泛型编程涉及到的知识点较广,例如:容器、迭代器、算法等都是泛型编程的实现实例。面试者可选择自己掌握比较扎实的一方面进行展开。\n\n容器:涉及到 STL 中的容器,例如:vector、list、map 等,可选其中熟悉底层原理的容器进行展开讲解。\n迭代器:在无需知道容器底层原理的情况下,遍历容器中的元素。\n模板:可参考本章节中的模板相关问题。\n\n","categories":["cpp-devel"]},{"title":"一些c++库的使用总结","url":"/posts/b945c282.html","content":"简介本文介绍一些c++库的使用。\ngflags简单实用gflags是一个流行的解析命令行的c++库。用户使用这个库定义的变量,可以通过多种途径进行赋值。\ngflags支持定义多种数据类型,包括bool、int32、int64、uint64、double、string。\n定义一个变量的操作如下:\nDEFINE_bool(name, "default value", description);\n\n然后在main函数开头添加 google::ParseCommandLineFlags(&argc, &argv, true);,在main函数末尾添加 google::ShutDownCommandLineFlags();。\n如果需要使用一个已经定义过的flags变量,则需要使用DECLARE_XXX,否则将会提示没有申明过这个变量。\ngflags提供参数校验器,使用RegisterFlagValidator函数注册校验器。\ngflags提供google::SetVersionString()函数设置版本信息(—version时返回的字符)。\ngflags提供gflags::SetUsageMessage函数设置帮助信息(—help时返回的字符)。\ngflags提供–flagfile参数指定配置文件,gflags将从这个配置文件中读取配置信息,同时,这个配置文件中也支持—flagfile读取配置。\ngflags提供—fromenv参数从环境变量中读取配置信息。\n编译实用gflags一个比较坑的地方是,默认情况下gflags库的namespace是flags,而一大批依赖这个库的第三方库使用的google名字空间下的gflags。为了解决这个问题,需要在编译时指定名字空间(添加参数-DGFLAGS_NAMESPACE=google)。\n有时候可能把gflags链接到动态链接库中,编译时的flags需要添加-fPIC,解决方法是在gflags中找到CMakeLists.txt中添加add_compile_options(-fPIC) 。\n默认情况下gflags使用Release编译的,为了方便调试,建议设置-DCMAKE_BUILD_TYPE=RelWithDebInfo,即设置-o2 -g\nglog使用它是一个使用方便的日志库,这个库是一个带buffer的同步日志,不宜打太大的日志,也不宜打请求级别的日志。如果需要打请求级别的日志,还是需要换成其他异步日志。\n使用起来极其方便,包含头文件(glog/logging.h),然后直接使用。\n需要注意的是,FATAL日志会出core。\nglog支持DLOG功能,这类日志在添加NDEBUG下不会打印DLOG日志,在没有这个flag下会打印这个日志,方便用户调试。\nglog支持VLOG功能,这类日志划分为多个级别,通过命令行参数控制打印的级别。\n使用时,在main函数开始添加google::InitGoogleLogging(argv[0]);开启使用glog之旅。\n编译glog依赖gflags库,使用glog的话建议先安装一下gflags,同时名字空间设置成google。\n编译glog之前,先指定gflags路径,使用参数CMAKE_INCLUDE_PATH和CMAKE_LIBRARY_PATH完成。\nrocksdb一个内嵌kv存储,常被用做各种分布式kv存储系统。它也可以用来简单存个数据,做个二级存储啥的,作为一次存储性能还是不够的,毕竟跳表的性能没法跟哈希表比。\n使用编译这个库依赖flags,所以编译前可以指定一下gflags,并添加参数-DWITH_GFLAGS=1指定依赖flags。\nfruit使用这是一个基于元编程实现的依赖注入库。\n编译这个库依赖boost,编译时需要指定boost的目录,使用参数-DBoost_INCLUDE_DIR=指定boost位置。\n这个库编译时建议添加-DBUILD_SHARED_LIBS=OFF关闭动态链接库。\nprotobuf老牌的序列化、反序列化工具,常被用于各种rpc框架,数据传输等。除此之外:\n\n可以序列化json。配合阿波罗实现一套强大的在线配置系统\n文本格式。一套简单实用的配置系统\n\n需要注意的是,基于bazel构建时默认开启编译优化;如果使用autogen.sh + configure构建时,需要手工添加”O2 -g”,不然就没开优化。\nfollyfacebook开源的重量级库,极大的补充了c++的“基础设施”。不足点,依赖比较多,编译起来比较费劲。\nabseilgoogle开源的,对c++stl的补充,功能较少,更多的是兼容语言特性,让c++11标准下用上c++17、c++20的特性。不足之处,代码不多,静态库一大堆,建议使用bazel或者cmake的find_package进行依赖打平,不然解决依赖问题就够麻烦的了。\nboostc++ stl的补充,stl的候选集。与folly、abseil相比,功能上 folly > boost > abseil,性能上folly = abseil > boost。编译安装、链接这块相对于folly、abseil容易。\nfmt字符串格式化库,部分已经进入c++20,纯头文件。\ncatch2单侧库,纯头文件,编译起来慢。\npoco面向应用的库,各种常用中间件、工具封装都有,不过性能一般,没在生产环境使用过。\n","categories":["cpp-devel"],"tags":["c++","lib"]},{"title":"生产环境使用boost::fiber","url":"/posts/589c6abb.html","content":"简介boost::fiber是一类用户级线程,也就是纤程。其提供的例子与实际生产环境相距较远,本文将对其进行一定的改造,将其能够投入到生产环境。同时由于纤程是具有传染性的,使用纤程的代码里也全部要用纤程封装,本文将对一些组件进行简单封装。\nfiber封装boost::fiber支持设置pthread和fiber的比例是1:n还是m:n,同时也支持设置调度方式是随机调度还是抢占调度。本文中选择使用抢占式调度,并且是m:n的比例,这种选择适用面更加广。既然pthread和fiber比例是m:n,那么这个m一般等于逻辑核数量,也就是需要设置fiber调度的线程控制在大小为固定的线程池中。fiber中抢占式调度方式也要求固定的线程池数量,外部前程加入时,可能会影响抢占式调度,即不能在外部线程中调用fiber,不然这个线程就加入到了fiber调度的pthread中了。\n这时,需要一个设置一个队列,外部线程往这个队列中添加任务;内部线程池从队列中取任务,同时触发fiber,在fiber中可以继续触发fiber。触发队列、内部队列、工作线程、外部线程的关系如下图所示: \n\n运行逻辑被装箱到一个任务中,然后被添加到任务队列,这一步利用模板和上转型实现,代码如下: \nclass IFiberTask {\n public:\n IFiberTask() = default;\n virtual ~IFiberTask() = default;\n\n IFiberTask(const IFiberTask& rhs) = delete;\n IFiberTask& operator=(const IFiberTask& rhs) = delete;\n IFiberTask(IFiberTask&& other) = default;\n IFiberTask& operator=(IFiberTask&& other) = default;\n\n virtual void execute() = 0;\n public:\n inline static std::atomic_size_t fibers_size {0};\n};\n\ntemplate <typename Func>\nclass FiberTask: public IFiberTask {\n public:\n explicit FiberTask(Func&& func) :func_{std::move(func)} { }\n\n ~FiberTask() override = default;\n FiberTask(const FiberTask& rhs) = delete;\n FiberTask& operator=(const FiberTask& rhs) = delete;\n FiberTask(FiberTask&& other) noexcept = default;\n FiberTask& operator=(FiberTask&& other) noexcept = default;\n\n void execute() override {\n fibers_size.fetch_add(1);\n func_();\n fibers_size.fetch_sub(1);\n }\n\n private:\n Func func_;\n};\nIFiberTask是任务基类,不可拷贝;FiberTask是模板类,成员变量func_存储算子。使用IFiberTask类指针指向特化后的FiberTask对象,这时就实现的装箱操作,调用execute时,实际调用了子类的execute,触发封装的func_对象。\n外部队列基于boost::fibers::buffered_channel实现,这是一个支持并发的队列,队列的元素类型为std::tuple<boost::fibers::launch, std::unique_ptr>,其中tuple第一元素存储任务的触发形式,进入队列还是立即触发。\n接着是任务装箱,代码如下:\ntemplate<typename Func, typename... Args>\nauto Submit(boost::fibers::launch launch_policy, Func&& func, Args&&... args) {\n // 捕获lambda极其参数\n auto capture = [func = std::forward<Func>(func),\n args = std::make_tuple(std::forward<Args>(args)...)]() mutable {\n return std::apply(std::move(func), std::move(args));\n };\n\n // 任务的返回值类型\n using task_result_t = std::invoke_result_t<decltype(capture)>;\n // 该任务packaged_task的\n using packaged_task_t = boost::fibers::packaged_task<task_result_t()>;\n // 创建任务对象\n packaged_task_t task {std::move(capture)};\n // 装箱到FiberTask中\n using task_t = fiber::FiberTask<packaged_task_t>;\n // 获取packaged_task的future\n auto result_future = task.get_future();\n // 添加到buffered_channel中\n auto status = work_queue_.push(\n std::make_tuple(launch_policy, std::make_unique<task_t>(std::move(task))));\n\n if (status != boost::fibers::channel_op_status::success) {\n return std::optional<std::decay_t<decltype(result_future)>> {};\n }\n\n return std::make_optional(std::move(result_future));\n}\n\n代码中,先捕获lambda表达式及其参数,获取返回值类型并添加到packaged_task中,然后装箱到FiberTask中,使用packaged_task获取future并返回,FiberTask对象添加到队列中,使用IFiberTask的指针指向这个对象,实现装箱操作。\n接着是内部任务触发的逻辑,首先创建一个线程池,每个线程注册调度器,接着从队列中获取任务,触发fiber。工作线程的执行函数如下:\n// 注册调度算法为抢占式调度\nboost::fibers::use_scheduling_algorithm<boost::fibers::algo::work_stealing>(threads_size_, true);\n// 创建task类型\nauto task_tuple = typename decltype(work_queue_)::value_type {};\n\n// 从队列中获取任务\nwhile(boost::fibers::channel_op_status::success == work_queue_.pop(task_tuple)) {\n // 解包\n auto& [launch_policy, task_to_run] = task_tuple;\n // 触发 fiber并detach\n boost::fibers::fiber(launch_policy, [task = std::move(task_to_run)]() {\n task->execute();\n }).detach();\n}\n\n抢占式调度在注册时需要指定线程池大小,这时不能在外部线程中调用fiber,因为调用fiber的时候会把该线程添加到fiber调度的线程中,也就调整了fiber的worker线程数量。\n以上代码实现了fiber触发器、任务队列、工作线程池等逻辑。理论上可以创建多个fiber调度组件对象,每个组件根据自己的需要设置资源情况。但实际应用中,还是建议使用一个全局调度组件,因为当A调度器中的任务依赖B调度器的任务的同时,就会出现阻塞工作线程,影响实际性能。\n下面封装一个全局调度器,提供递交任务的接口和结束调度的接口:\nclass DefaultPool {\n private:\n static auto* Pool() {\n const static size_t size = std::thread::hardware_concurrency();\n static fiber::FiberPool pool(size, size*8);\n return &pool;\n }\n\n public:\n template<typename Func, typename... Args>\n static auto SubmitJob(boost::fibers::launch launch_policy, Func &&func, Args &&... args) {\n return Pool()->Submit(launch_policy, std::forward<Func>(func), std::forward<Args>(args)...);\n }\n\n template<typename Func, typename... Args>\n static auto SubmitJob(Func &&func, Args &&... args) {\n return Pool()->Submit(std::forward<Func>(func), std::forward<Args>(args)...);\n }\n\n static void Close() {\n Pool()->CloseQueue();\n }\n\n private:\n DefaultPool() = default;\n};\n\n其他组件封装上面对boost::fiber进行封装,得到一个能投入生产环境的调度器。但是仅仅是这些是不够的,毕竟对于生产环境中的服务而言,外部服务、中间件的依赖是不能少的。纤程是具有传染性的,对于外部组件提供的sdk,发送请求并进行同步等待会阻塞纤程对应的工作线程,影响整套机制。为此,需要对现有的组件进行封装,对于同步接口,需要使用线程池配合fiber::promise;对于异步接口,可以改造成fiber::promise、future机制。下面介绍几种常用组件的fiber封装。\nredis客户端封装同步接口加线程池的方式将同步接口改造成异步接口的方案,存在较大的安全隐患。线程池的容量不可控,当流量突然增加时,需要大量线程去等待,从而耗尽线程池资源,造成任务大量积压,服务崩溃。而扩大线程池数量,又消耗了大量的资源。\n综上,对于fiber化封装,还是建议采用异步接口。hiredis库支持异步接口,redis_plus_plus库对hiredis进行了c++封装,同时也提供了异步接口,本节将面向这个接口进行改造。\nredis提供了挺多的接口,这里只对del、get、set三个接口做个示范:\ntemplate<typename Type>\nusing Promise = boost::fibers::promise<Type>;\n\ntemplate<typename Type>\nusing Future = boost::fibers::future<Type>;\n\nFuture<long long > Del(const StringView &key) {\n auto promise = std::make_unique<Promise<long long >>();\n auto future = promise->get_future();\n // 在回调函数中对promise赋值\n redis_.del(key, [promise =promise.release()](sw::redis::Future<long long > &&fut) mutable {\n try {\n promise->set_value(fut.get());\n } catch (...) {\n promise->set_exception(std::current_exception());\n }\n delete promise;\n });\n return future;\n}\n\nFuture<OptionalString> Get(const StringView &key) {\n auto promise = std::make_unique<Promise<OptionalString>>();\n auto future = promise->get_future();\n // 在回调函数中对promise赋值\n redis_.get(key, [promise = promise.release()](sw::redis::Future<OptionalString> &&fut) mutable {\n try {\n promise->set_value(fut.get());\n } catch (...) {\n promise->set_exception(std::current_exception());\n }\n delete promise;\n });\n return future;\n}\n\nFuture<bool> Set(const StringView &key, const StringView &val) {\n auto promise = std::make_unique<Promise<bool>>();\n auto future = promise->get_future();\n // 在回调函数中对promise赋值\n redis_.set(key, val, [promise = promise.release()](sw::redis::Future<bool> &&fut) mutable {\n try {\n promise->set_value(fut.get());\n } catch (...) {\n promise->set_exception(std::current_exception());\n }\n delete promise;\n });\n return future;\n}\n\n注意,redis_plus_plus对每个回调函数通过模板进行判断,因此无法使用mutable+移动捕获promise,只能使用指针赋值的方式实现。redis_plus_plus在1.3.6以后的版本才有回调函数机制,之前的版本不支持。上面原理是,创建fiber的promise和future,然后让redis的回调函数中捕获promise,并在promise中对数据进行赋值。而外部使用fiber的future进行等待,并不会阻塞工作线程。\ngrpc客户端封装跟上面的redis客户端类似,这里也建议对grpc的异步客户端进行改造,支持fiber的promise、future机制。grpc的异步客户端需要牵扯到grpc::CompletionQueue,里面实现了一套poll engine,需要绑定一个线程去进行epoll_wait操作。首先定义一个GrpcClient类,包含四个成员变量、两个成员函数,如下所示:\nclass GrpcClient {\n public:\n explicit GrpcClient(const ClientOption& option);\n ~GrpcClient();\n\n // 对外提供的接口\n Future<meta::HelloResponse> Call(const meta::HelloRequest& request);\n\n private:\n // worker线程执行的逻辑\n void Work();\n\n private:\n std::unique_ptr<grpc::CompletionQueue> completion_queue_;\n std::thread worker_;\n std::shared_ptr<grpc::Channel> channel_;\n gpr_timespec timespec_{};\n};\n\n异步客户端分为三个部分逻辑,第一个是请求发送(Call函数),第二个是io线程批量处理,第三个是外部等待Future。为了能够让io线程里给Promise进行赋值,需要Call函数中将Promise及其相关上下文传递到io线程中,这里定义一个上下文结构体:\nstruct CallData {\n grpc::ClientContext context; // grpc上下文\n Promise<meta::HelloResponse> promise; // Promise对象\n grpc::Status status; // grpc调用状态\n meta::HelloResponse response; // 相应包\n};\n\nCall函数中的逻辑如下:\n// 创建上下文对象\nauto data = new CallData;\n// 设置超时时间\ndata->context.set_deadline(timespec_);\n// 创建桩\nmeta::HelloService::Stub stub(channel_);\nauto future = data->promise.get_future();\n// 异步调用,添加到完成队列中\nauto rpc = stub.Asynchello(&data->context, request, completion_queue_.get());\n// 绑定response、status,并将上下文对象作为tag传下去\nrpc->Finish(&data->response, &data->status, reinterpret_cast<void*>(data));\nreturn future;\n\ndata对象在该函数中创建,在Work函数中释放,不存在内存泄漏问题。grpc的异步稍微有点麻烦,发送之后,还要绑定数据。接着是Work线程中的逻辑了:\nCallData* data = nullptr;\nbool ok = false;\n// 获取状态完毕的数据\nwhile (completion_queue_->Next((void**)&data, &ok)) {\n // 判断队列是否已经结束\n if (!ok) {\n break;\n }\n // 如果grpc状态ok,则赋值\n if (data->status.ok()) {\n data->promise.set_value(std::move(data->response));\n } else {\n // 否则设置异常\n data->promise.set_exception(std::make_exception_ptr(\n std::runtime_error(data->status.error_message())));\n }\n // 删除数据\n delete data;\n data = nullptr;\n}\n\n调用完成队列的Next函数会阻塞,如果队列中存在状态达到最终状态的数据,则返回一条。从完成对于中取到的数据的顺序与入队顺序不同。\n上面两个函数组合实现了Future获取和Promise赋值的操作,使得grpc客户端能在fiber中使用。\n参考\nfiberpool代码\n生产环境使用fiber\ngrpc异步客户端\nhiredis\nredis_plus_plus\n\n","categories":["cpp-devel"],"tags":["boost","协程","fiber","纤程","redis","hiredis","grpc","redis_plus_plus"]},{"title":"folly_intra","url":"/posts/71ae45ce.html","content":"","categories":["cpp-devel"],"tags":["c++","folly","cpp"]},{"title":"glog_in_using","url":"/posts/c2b35ce7.html","content":"","categories":["cpp-devel"]},{"title":"使用jeprof定位内存泄漏","url":"/posts/361305b0.html","content":"简介本文将介绍基于jeprof定位内存泄漏问题,支持缓慢内存泄漏场景、瞬间oom场景等。\njeprof介绍jeprof是jemalloc提供的一个内存优化的工具,jemalloc是facebook开源的内存管理工具,类似ptmalloc和tcmalloc,在多线程场景具有较好的性能。默认情况下编译jemalloc后并没有jeprof工具,需要在编译时添加--enable-prof参数,然后在编译目录的bin目录中就能找到jeprof程序。开启prof功能的jemalloc根据环境变量MALLOC_CONF和mallctl接口操作prof功能。MALLOC_CONF变量接收的参数参考jemalloc prof功能。\n缓慢内存泄漏定位方案这个方案适用于服务内存泄漏不是特别快,从服务启动完毕到oom的时间大于30秒以上,开发者有充足的时间去触发dump。这个方案是在服务中添加dump接口,然后让开发在服务启动完毕(数据字典、索引加载完毕)后,触发一次dump,然后刷一定流量后,再触发一次dump,对比两次dump前后内存差,确定内存泄漏的位置。\n需要注意的是,这种方法一定要服务启动完毕,不然服务启动过程中加载的数据就会在diff结果中,影响判断。其次,这种方法需要要求开发者把服务里的cache关闭,cache的存在也会影响diff结果的判断。\n示例服务介绍示例服务是一个基于beast开发的一个webservice,使用cmake构建,使用conan管理依赖,代码可以在github地址上找到。这个webservice提供两个接口,一个是正常返回数据的接口,一个是内存泄漏的接口。内存泄漏的接口用于模拟内存泄漏的场景。采用http协议的服务是因为http服务测试起来方便,不需要手写客户端。\n项目的conan配置(conanfile.txt)内容如下:\n[build_requires]\n\n[requires]\nboost/1.77.0\njemalloc/5.2.1\nglog/0.5.0\n\n[generators]\ncmake\n\n[options]\njemalloc:enable_prof=True\nbeast是boost中的一个库,glog是日志库,下文用来分析内存泄漏的工具jeprof在jemalloc库中,需要注意的是options中jemalloc的prof已经要手工开启,默认是不开启的。\n这个webservice参考beast的“高级服务器”代码实现的,正常的接口的路径是“/”,逻辑如下:\nstd::string HttpSession::processNormal() {\n LOG(INFO) << "visit";\n return "use http://www.webgraphviz.com/ ";\n}\n其功能就是打个日志,然后输出一句话。\n内存泄漏的接口路径为”/leak”,其逻辑如下:\nstd::string HttpSession::processLeak() {\n int32_t* leak = new int32_t[1024*256];\n LOG(INFO) << "leak address " << leak;\n return "memory leak";\n}\n这个接口调用一次就泄漏1MB的内存。\n这个项目编译运行的步骤:\n# 创建编译目录\nmkdir build;\ncd build;\n# 使用conan安装依赖\nconan install ..\n# 使用cmake构建\ncmake ..\n# 编译\nmake\n# 运行\n./bin/server\n\ndump接口开发对于一些泄漏缓慢服务,使用定量dump的方式只会带来一堆的dump文件或者连dump文件都没有,影响泄漏定位效率。为此,需要一种简单的手工触发dump的方式来协助定位内存泄漏。\n在现有服务中,添加一个手工触发dump的接口,即在新的接口里调用jemalloc的dump函数:\nmallctl("prof.dump", nullptr, nullptr, nullptr, 0)\n\nmallctl函数是操作调用函数,”prof.dump”是进行内存情况dump操作。\n实际代码可以参考github代码:\nstd::string HttpSession::processDump() {\n if (mallctl("prof.dump", nullptr, nullptr, nullptr, 0) == 0) {\n LOG(INFO) << "dump sucess";\n return "dump success";\n } else {\n LOG(INFO) << "dump sucess";\n return "dump fail";\n }\n}\n\n上面接口为”/dump”。\n接口使用与数据分析服务启动前设置环境变量MALLOC_CONF='prof:true,prof_prefix:jemalloc' 开启prof功能。\n服务启动并加载完数据后,调用dump接口生成一份基准的内存分析数据,在本例子中,使用下面命令即可:\ncurl 'http://localhost:8080/dump';\n\n然后调用内存泄漏的接口,模拟内存泄漏,命令如下:\ncurl 'http://localhost:8080/leak';\n\n多次调用内存泄漏的接口之后,再次调用dump接口,生成第二份内存分析数据。这时我们得到了两份数据,第一份是基准,文件名是“jemalloc.10391.0.m0.heap”,第二份是“jemalloc.10391.1.m1.heap”。接着我们使用jeprof工具来分析这两份数据,jeprof可以从conan下载的jemalloc中找到,命令如下:\njeprof --dot ./bin/server --base=jemalloc.10391.0.m0.heap jemalloc.10391.1.m1.heap\n\n终端中会输出dot语法的图,将其贴到在线dot绘图网站,生成内存分配图,然后进行分析。\n样例数据可以在这里找到,生成的图如下:\n\n从图中可以看到,两个快照比较多余部分(泄漏部分)的内存在”processLeak”函数中。\n快速oom定位方案有时候某个流量触发了服务中的一个死循环,然后死循环里会申请内存,但这份内存需要在循环外的其他地方才会释放。这时,服务就会出现瞬间oom的情况,速度之快,无法人工dump。\n面对这种情况,就要开启定量dump和推出dump,需要的参数如下:\n\n\n\n参数\n含义\n\n\n\nprof:true\n启动profile\n\n\nprof_final:true\n表示退出时prof\n\n\nlg_prof_interval:N\n每流转 1 « N 个字节,将采样统计数据转储到文件\n\n\nprof_gdump:true\n打到新高dump\n\n\nprof_active:false\n不激活,用于手工激活\n\n\nlg_prof_sample:N\n平均每分配出 2^N 个字节 采一次样。当 N = 0 时,意味着每次分配都采样\n\n\n备注: lg_prof_sample是分配计数的采样频率,lg_prof_interval是分配统计,只有被计数了才会被打印。\n建议的prof环境变量设置:\nexport MALLOC_CONF="prof_leak:true,lg_prof_sample:0,prof_final:true;lg_prof_interval:30"\n\n每分配1GB内存就打印一份数据,并且在服务退出时,也同时dump一份。然后比较每份dump的数据的diff确认内存泄漏的位置。\n需要注意的是,这种方法也没法处理cache问题。\n参考\njemalloc prof功能\ndot可视化\njemalloc的heap profiling\n\n","categories":["cpp-devel"],"tags":["c++","boost","jemalloc","jeprof","memory leak"]},{"title":"日志代码收敛(log4cpp/glog)","url":"/posts/b270d3a9.html","content":"简介本文介绍某项目中日志代码的收敛工作.这个项目由多个模块组成, 各个模块使用的日志系统不同, 有的模块使用log4cpp, 有的模块使用glog.这给项目的管理带来了一定的困难, 因此需要对整个项目进行改造, 统一日志系统.\n背景日志库目前c++常用并且开源的日志库包括:\n\nlog4cxx\nlog4cpp\nlog4cplus\nglog\ng3log\nboost.log\nboost.log v2\n\nlog4cxxlog4cxx是apache的log4j的官方c++实现, 架构类似于log4j, 使用跟log4j兼容的配置文件.这个项目已经不再更新.在centos的软件库中包含了log4cxx的开发包(log4cxx-devel).\nlog4cpplog4cpp是log4j的一个非官方实现, 与log4j类似的架构, 并且兼容log4j的配置文件.这个项目最新更新的时间是2017, 并且已经一年没更新了.在centos的软件库中包含了log4cpp的开发包(log4cpp-devel).\nlog4cpluslog4cplus是log4j的另一个非官方实现, 与log4j类似的架构, 并且兼容log4j的配置文件.这个项目一直在更新, 并且已经发布2.0版本.在centos的软件库中包含了log4cplus的开发包(log4cplus)\nglogglog是谷歌开源的日志系统, 以简单著称, 支持的功能包括:\n1, 参数设置,以命令行参数的方式设置标志参数来控制日志记录行为;2, 严重性分级,根据日志严重性分级记录日志;3, 可有条件地记录日志信息;4, 条件中止程序。丰富的条件判定宏,可预设程序终止条件;5, 异常信号处理。程序异常情况,可自定义异常处理过程;6, 支持debug功能。可只用于debug模式;7, 自定义日志信息;8, 线程安全日志记录方式;9, 系统级日志记录;10, google perror风格日志信息;11, 精简日志字符串信息。\ng3logglog不支持异步日志, 并且性能较差. 于是有开发者基于glog开发了支持异步日志的新日志系统, 命名为g3log.\nboost.log v1这个日志系统出现较早, 但是一直没有合入到boost套装中.用户如果要使用这个库, 则需要单独编译.\nboost.log v2从boost 1.54开始, boost.log v2加入boost库, 但是目前centos7的boost版本是1.53.boost将日志系统进行分层, 类似于log4j, 并且同时支持同步和异步日志.\n项目目前项目的A模块使用log4cpp作为日志\n","categories":["cpp-devel"]},{"title":"基于protobuf的配置系统","url":"/posts/cd6cc41b.html","content":"简介本文介绍一种基于protobuf实现的配置文件体系,提升配置文件使用体验,同时支持分环境加载不同配置。本配置系统相对于json,支持配置中添加注释,方便理解;支持自动反序列化,操作简单,同时减少DOM操作不当造成的coredump。本配置系统相对于yml等配置文件,支持自动反序列化;不会因为缩进问题造成解析失败。\n使用说明基础使用基础使用的步骤包括proto定义、配置文件编写、服务引入三个环节。首先是proto定义,参考下面定义一个proto:\nsyntax="proto3";\n\nmessage LogConfig {\n enum LogLevel {\n DEBUG = 0;\n NOTICE = 1;\n WARNING = 2;\n ERROR = 3;\n FATAL = 4;\n }\n string path = 1;\n LogLevel level = 2;\n}\n\nmessage ServiceConfig {\n string config = 1;\n}\n\nmessage ServerConfig {\n uint32 port = 1;\n uint32 thread_num = 2;\n LogConfig log_config = 3;\n repeated ServiceConfig service_config = 4;\n}\n\n接着是服务配置编写,例如:\nport : 8080\nthread_num: 8\nlog_config: {\n path: "./log"\n level: NOTICE\n}\nservice_config: {\n config: "123"\n}\n\n接着是在服务中使用了,由于本配置体系依赖了protobuf、glog、gflags,使用时确保链接这三个库。\n使用代码如下:\n#include <gflags/gflags.h>\n#include <glog/logging.h>\n#include "config_manager.h"\n#include "configs/server_config.pb.h"\n\nint main(int argc, char** argv) {\n gflags::ParseCommandLineFlags(&argc, &argv, true);\n google::InitGoogleLogging(argv[0]);\n std::string file = "../conf/server";\n ServerConfig serverConfig;\n ConfigManager::parse(file, &serverConfig);\n google::ShutdownGoogleLogging();\n return 0;\n}\n\n其中配置路径不需要后缀,默认配置文件后缀为”*.conf”,也就是”../conf/server.conf”文件。ServerConfig为目标配置反序列化之后的对象。\n高级使用这部分介绍用户可以根据自己的需要,针对不同环境使用不同的配置,例如prod、pre、test使用不同的配置文件,同时有一份基准配置,优先使用指定环境的配置,如果该配置未设置,使用基准配置覆盖。使用gflag “env”确定加载哪份配置作为环境配置,默认是prod。使用gflag “suffix”确定配置文件后缀,默认是”conf”。\n例如,基准配置conf/server.conf是:\nport : 8080\nthread_num: 8\nlog_config: {\n path: "./log"\n level: NOTICE\n}\nservice_config: {\n config: "123"\n}\n\n环境配置conf/server-prod.conf是:\nport : 80\nlog_config: {\n level: WARNING\n}\n\n最终得到的合并配置为:\nport: 80 \nthread_num: 8 \nlog_config { \n path: "./log" \n level: WARNING \n} \nservice_config { \n config: "123" \n}\n\n需要注意的是,如果一个字段是repeated时,如果环境配置没有设置,就使用基准配置的;如果环境配置设置了,就不会使用基准配置了。\n实现原理文本配置解析基于protobuf的文本配置,利用的是google/protobuf/text_format.h文件中的google::protobuf::TextFormat::Parse函数,其功能是将文本内容反序列化为message内容。这个函数其实是google::protobuf::Message的DebugString函数的逆操作。\n解析的代码如下:\nint file = open(path.c_str(), O_RDONLY);\nif (file < 0) {\n return false;\n}\ngoogle::protobuf::io::FileInputStream reader{file};\nreader.SetCloseOnDelete(true);\nif (!google::protobuf::TextFormat::Parse(&reader, msg)) {\n return false;\n}\nreturn true;\n\n\n配置合并protobuf提供了MergeFrom函数支持两个Message对象进行合并,对于repeated的结构,合并后的结果是这两个结构的并集。这个情况是不符合预期的,例如Message里有a字段是repeated的,合并结果如下:\n\n\n\nA 配置中a字段元素数量\nB配置中a字段元素数量\n合并结果\n\n\n\n2\n0\n2(全部来自A)\n\n\n0\n2\n2(全部来自B)\n\n\n2\n3\n2(全部来自A)\n\n\n针对这样的需求,不能直接使用MergeFrom函数,需要使用protobuf的反射能力进行逐一赋值。\n代码有点长,可以参考代码。\n","categories":["cpp-devel"],"tags":["c++","protobuf"]},{"title":"spinlock","url":"/posts/4a74a9d.html","content":"简介本文将介绍自旋锁的原理, 并封装linux提供的自旋锁, 最后介绍boost提供的自旋锁.\n背景自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,即在标志寄存器中关闭/打开中断标志位,不需要自旋锁)。\n何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。\n跟互斥锁一样,一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。由此我们可以看出,自旋锁是一种比较低级的保护数据结构或代码片段的原始方式,这种锁可能存在两个问题:死锁。试图递归地获得自旋锁必然会引起死锁:递归程序的持有实例在第二个实例循环,以试图获得相同自旋锁时,不会释放此自旋锁。在递归程序中使用自旋锁应遵守下列策略:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。此外如果一个进程已经将资源锁定,那么,即使其它申请这个资源的进程不停地疯狂“自旋”,也无法获得资源,从而进入死循环。过多占用cpu资源。如果不加限制,由于申请者一直在循环等待,因此自旋锁在锁定的时候,如果不成功,不会睡眠,会持续的尝试,单cpu的时候自旋锁会让其它process动不了. 因此,一般自旋锁实现会有一个参数限定最多持续尝试次数. 超出后, 自旋锁放弃当前time slice. 等下一次机会。由此可见,自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。但是如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP(多处理器)的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。上面简要介绍了自旋锁的基本原理,以下将给出具体的例子,进一步阐释自旋锁在实际系统中的应用。上面我们已经讲过自旋锁只有在内核可抢占或SMP(多处理器)的情况下才真正需要,下面我们就以SMP为例,来说明为什么要使用自旋锁,以及自旋锁实现的基本算法。\n通俗的讲, 自旋锁是一种不放弃CPU资源的锁.\n自旋锁使用封装class SpinLock {\npublic:\n void lock() {\n pthread_spin_lock(&spinlock_);\n }\n\n void unlock() {\n pthread_spin_unlock(&spinlock_);\n }\n\nprivate:\n pthread_spinlock_t spinlock_;\n};\n\nclass SpinLockGuard {\npublic:\n explicit SpinLockGuard(SpinLock &spinlock): spinlock_(spinlock) {\n spinlock_.lock();\n }\n\n ~SpinLockGuard() {\n spinlock_.unlock();\n }\n\n};\n\n\nSpinLock lock;\n{\n SpinLockGuard guard(lock);\n}\n\n\n\n\n\nboost版本boost中实现了自旋锁功能, 但是没有显示提供, 自旋锁的位置在boost/smarty_ptr/detail/spinlock.hpplockguard在<boost/thread/thread_guard.hpp>.\n使用方法如下:\nusing SpinLock = boost::detail::spinlock;\nusing SpinLockGuard = boost::lock_guard<SpinLock>;\n\n\nSpinLock lock;\n{\n SpinLockGuard guard(lock);\n}\n\n\n\n\n\n\n\n\n","categories":["cpp-devel"],"tags":["boost","cpp","spin lock","自旋锁"]},{"title":"talk_concurrent_hashmap","url":"/posts/637f3764.html","content":"简介Folly 是 Facebook 开源的 C++ 基础库,其中提供了两种高性能并发哈希表:\n\nAtomicHashMap:基于 CAS + 开放寻址,适合 int32/int64 键的高并发场景\nConcurrentHashMap:基于分片锁 + hazard pointer,支持任意键值类型\n\n本文深入解析两者的底层实现。\nConcurrentHashMap 架构ConcurrentHashMap 是 Folly 中功能最完整的并发哈希表,与 std::unordered_map 接口相近,但线程安全。\n模板参数template <\n typename KeyType,\n typename ValueType,\n typename Hash = std::hash<KeyType>,\n typename KeyEqual = std::equal_to<KeyType>,\n typename Allocator = std::allocator<uint8_t>,\n uint8_t ShardBits = 8, // 分片位数\n typename BucketT = folly::MicroLock,\n bool ApplyPriority = false\n>\nclass ConcurrentHashMap;\n\n关键参数:\n\nShardBits:控制分片数量,默认 8,即 256 个 Segment\nBucketT:每个 bucket 的锁类型,默认用 MicroLock(4 字节自旋锁)\n\n分片锁(Sharded Locking)设计这是 ConcurrentHashMap 最核心的设计思想。\n┌─────────────────────────┐\n│ ConcurrentHashMap │\n│ │\n│ Segment[0] │\n│ ┌─────────────────┐ │\n│ │ Bucket[0] lock │ │\n│ │ Bucket[1] lock │ │\n│ │ Bucket[2] lock │ │\n│ │ ... │ │\n│ └─────────────────┘ │\n│ Segment[1] │\n│ ┌─────────────────┐ │\n│ │ Bucket[0] lock │ │\n│ │ Bucket[1] lock │ │\n│ │ ... │ │\n│ └─────────────────┘ │\n│ Segment[2..255] │\n└─────────────────────────┘\n\n设计要点:\n\n两级分片:ShardBits 决定 Segment 数量,每个 Segment 内再分 Bucket\n默认 256 个 Segment:哈希值的高 ShardBits 位决定落到哪个 Segment,低位决定 Bucket\n每 Bucket 一把锁:锁粒度细到 bucket 级别,最大程度减少竞争\nMicroLock:仅 4 字节的自旋锁,嵌入在 bucket 结构中,零额外内存开销\n\n哈希值的使用hash_value = Hash(key);\n\n// 高 ShardBits 位 → Segment 索引\nsegment_idx = hash_value >> (64 - ShardBits);\n\n// 剩余低位 → Bucket 索引(取模 Segment 内 bucket 数)\nbucket_idx = lower_bits % num_buckets_per_segment;\n\n这样设计的好处是:同一个 Segment 内的 bucket 有相似的高位哈希值,理论上会竞争同一把 Segment 级别的资源,但实际的锁粒度在 Bucket 级,避免了热点。\n并发操作实现读取(find)读取完全无等待(wait-free),不获取任何锁:\n\n哈希定位到 Segment + Bucket\n遍历 bucket 链表,原子读取节点\n通过 hazard pointer 保护正在读取的节点不被回收\n\n// 伪代码:读取路径\nauto result = find(key);\n// 1. 计算 hash\n// 2. 定位 segment[bucket_idx]\n// 3. 遍历链表,原子读每个节点的 key\n// 4. 匹配成功 → 返回值或引用\n// 5. 匹配失败 → 返回 end()\n// 全程无锁!\n\n读取路径不使用锁的关键在于 hazard pointer:读线程将正在访问的节点指针发布到 hazard pointer 列表,删除线程在回收节点前检查 hazard pointer,确保不会释放正在被读的节点。\n插入(insert / insert_or_assign)插入需要获取 bucket 级别的锁:\n\n哈希定位到 Segment + Bucket\n获取该 Bucket 的 MicroLock\n遍历链表查找 key\n存在 → 更新 value(insert_or_assign 模式)\n不存在 → 创建新节点插入链表头部\n\n\n释放锁\n\n// 伪代码:插入路径\nauto result = insert(key, value);\n// 1. 计算 hash,定位 bucket\n// 2. lock(bucket.microLock)\n// 3. 遍历链表,查找 key\n// 4. 如果存在 → 返回 {iterator, false}\n// 5. 如果不存在 → new Node{key, value},插入链表头\n// 6. unlock(bucket.microLock)\n// 7. 返回 {iterator, true}\n\n删除(erase)同样获取 bucket 锁,但涉及 hazard pointer 安全回收:\n\n定位 Bucket,获取锁\n遍历链表找到目标节点\n从链表中摘除节点\n释放锁\n将节点放入 retire 列表(延迟回收)\n当确定没有读线程持有该节点的 hazard pointer 时,安全释放内存\n\n// 伪代码:删除路径\nauto result = erase(key);\n// 1. 定位 bucket,lock\n// 2. 找到节点,从链表摘除\n// 3. unlock\n// 4. retire(node) // 加入待回收列表\n// 5. reclaim() // 检查 hazard pointers,安全释放\n\nassign_if_equal(原子条件更新)这是 ConcurrentHashMap 的独特方法,允许原子地进行”如果当前值等于期望值,则更新为新值”:\n// 如果 map[key] == expected,则 map[key] = desired\n// 整个操作在锁保护下原子完成\nauto result = map.assign_if_equal(key, expected, desired);\n\n这在需要 CAS 语义的并发场景中非常实用,比如:\n\n状态机的原子状态转换\n引用计数的线程安全更新\n\nHazard Pointer 内存回收传统并发哈希表的一个痛点:删除节点时,可能有读线程正在访问该节点。直接删除会导致 use-after-free。\nConcurrentHashMap 使用 hazard pointer 解决:\n读线程 删除线程\n──────── ────────\n读取节点 A 删除节点 A\n↓ ↓\nhazptr.protect(&A) 从链表摘除 A\n↓ ↓\n读取 A 的数据... retire(A) → 放入退休列表\n↓ ↓\nhazptr.clear() 扫描所有读线程的 hazard pointer\n ↓\n 如果 A 不在任何 hazptr 中\n ↓\n delete A\n\n关键点:\n\n读线程通过 hazard pointer 声明”我正在用这个节点”\n删除线程等待所有对节点的引用消失后才真正释放\n实现了安全的、无锁的读路径\n\nAtomicHashMap 架构AtomicHashMap 是 Folly 中另一种并发哈希表的实现,采用完全不同的设计哲学。\n核心设计AtomicHashMap 由多个 AtomicHashArray (AHA) 子表串联而成:\n\n初始只有一个 AHA\n填满后追加新的 AHA,形成子表链表\n元素永不移动:扩容通过追加而非 rehash\n\n┌─────────────────┐ ┌─────────────────┐\n│ AtomicHashArray │ →──→│ AtomicHashArray │ →──→ ...\n│ (Submap 0) │ │ (Submap 1) │\n│ [slot0] │ │ [slot0] │\n│ [slot1] │ │ [slot1] │\n│ ... │ │ ... │\n└─────────────────┘ └─────────────────┘\n\nAHA:开放寻址 + CAS每个 AHA 是固定大小的开放寻址哈希表,核心操作都用 CAS 原子完成:\n插入:\n\n哈希取模定位到 slot\nCAS 将 slot 的 key 从 EMPTY 改为 LOCKED(特殊的哨兵值)\nCAS 成功 → 写入 value → 将 key 从 LOCKED 改为真实 key(解锁)\nCAS 失败 → 线性探测下一个 slot\n\n查找(无锁):\n\n哈希定位 + 线性探测\n读取 slot key,比较匹配 / 空 / 继续探测\n完全无等待,因为元素始终处于有效状态\n\n删除:\n\n找到 slot\nCAS 将 key 改为 ERASED(墓碑值)\n墓碑永不回收,这是该设计的最大代价\n\n限制\n\n\n特性\nAtomicHashMap\nConcurrentHashMap\n\n\n\n键类型\n仅 int32/int64\n任意类型\n\n\n删除后内存回收\n永不回收(墓碑)\nHazard Pointer 回收\n\n\n扩容方式\n追加子表(性能递减)\n可重新分配\n\n\n读并发\nWait-free\nWait-free\n\n\n写并发\nKey 级 CAS\nBucket 级锁\n\n\n迭代器稳定性\n永远有效\nHazard Pointer 保证\n\n\n为什么 AtomicHashMap 更快(特定场景)\n读路径无需任何原子指令,直接内存读取\n写路径仅 CAS,无锁开销\n无内存分配/释放(墓碑机制)\n适合键不删除或删除很少的场景\n\n性能对比以下是 8 线程、100 万条目的基准对比:\n\n\n\n操作\nConcurrentHashMap\nAtomicHashMap\ntbb::concurrent_hash_map\n\n\n\n插入(50% 负载)\n—\n0.19 μs\n~0.5 μs\n\n\n查找(50% 负载)\n—\n0.05 μs\n~0.15 μs\n\n\n读并发\nWait-free\nWait-free\n有锁\n\n\nAtomicHashMap 比 tbb::concurrent_hash_map 快 2-5 倍。\n使用建议选 ConcurrentHashMap 的场景:\n\n键类型不是整数\n需要频繁增删\n需要 assign_if_equal 原子条件更新\n需要类似 std::unordered_map 的接口\n\n选 AtomicHashMap 的场景:\n\n键是 32/64 位整数\n删除极少或从不删除\n对读/写延迟要求极高的场景\n能接受固定的哨兵键值(EMPTY、LOCKED、ERASED)\n\n总结Folly 的两套并发哈希表体现了两种经典的并发设计思路:\n\nConcurrentHashMap:分片锁 + hazard pointer,通用性强,适合大多数场景\nAtomicHashMap:CAS + 开放寻址 + 追加子表,极致性能,适用于特定约束场景\n\n共同的设计精髓是 读路径无锁:通过精心设计的并发协议(hazard pointer / 原子状态机),让最频繁的读操作完全不阻塞,从而在高并发下获得近乎线性的扩展性。\n","categories":["cpp-devel"],"tags":["c++","folly","concurrent-hashmap","并发数据结构"]},{"title":"C++ 并发 LRU Cache 实现对比","url":"/posts/1c2ec8f7.html","content":"简介LRU Cache 是最常用的本地缓存淘汰策略。虽然经典的 std::list + std::unordered_map 实现只需 50 行代码,但在并发场景下,线程安全和性能优化会带来显著的复杂度。本文调研了当前 C++ 生态中几个有代表性的 LRU Cache 实现,从数据结构、并发策略、淘汰机制等维度进行对比。\n经典实现:list + unordered_map最基础的 LRU Cache 用双向链表维护访问顺序,哈希表提供 O(1) 查找。每次 get/put 时将被访问的条目移动到链表头部,容量满时淘汰链表尾部的条目。\ntemplate <typename K, typename V>\nclass SimpleLRUCache {\n using ListIter = typename std::list<std::pair<K, V>>::iterator;\n\n size_t capacity_;\n std::list<std::pair<K, V>> list_;\n std::unordered_map<K, ListIter> map_;\n mutable std::shared_mutex mtx_;\n\npublic:\n std::optional<V> get(const K& key) {\n std::unique_lock lock(mtx_);\n auto it = map_.find(key);\n if (it == map_.end()) return std::nullopt;\n // 移到链表头部\n list_.splice(list_.begin(), list_, it->second);\n return it->second->second;\n }\n\n void put(const K& key, const V& value) {\n std::unique_lock lock(mtx_);\n auto it = map_.find(key);\n if (it != map_.end()) {\n it->second->second = value;\n list_.splice(list_.begin(), list_, it->second);\n return;\n }\n if (list_.size() >= capacity_) {\n map_.erase(list_.back().first);\n list_.pop_back();\n }\n list_.emplace_front(key, value);\n map_[key] = list_.begin();\n }\n};\n\n优点:\n\n代码少,易于理解和维护\nstd::list::splice 不分配新节点,移动操作 O(1)\n\n缺点:\n\nstd::list 每次插入都分配堆内存(Node 分配),cache-unfriendly\nstd::shared_mutex 在极高并发下成为瓶颈\n链表中每个节点存储 pair<K,V>,存在额外的指针追踪开销\n\n适合缓存量不大(几千以内)、并发度不高的场景。\nfolly::EvictingCacheMap — 侵入式链表Facebook Folly 库中的 EvictingCacheMap 是单线程场景下最经典的 LRU Map 实现。它通过 Boost.Intrusive 将 LRU 链表指针直接嵌入到 value 对象内部,消除了独立的链表节点分配。\n核心设计:一个对象同时属于两个容器EvictingCacheMap 使用了 Boost.Intrusive 的多重继承机制,让每个缓存条目既是一个哈希表条目,又是一个 LRU 链表节点:\nstruct Node\n : public boost::intrusive::unordered_set_base_hook<safe_link>, // 哈希桶链指针\n public boost::intrusive::list_base_hook<safe_link> // LRU 链表指针\n{\n std::pair<const K, V> pr;\n};\n\n\nunordered_set_base_hook 在 Node 内部嵌入了哈希桶碰撞链的 prev/next 指针\nlist_base_hook 在 Node 内部嵌入了 LRU 双向链表的 prev/next 指针\nNode 本身是一个 pair<K,V>,不需要额外的包装\n\n这意味着:一次 new 分配,Node 同时出现在哈希表中和 LRU 链表中。相比于 list<pair<K,V>> 的方式,省去了为每个条目分配独立 list::node 的开销,内存布局更加紧凑,缓存友好性更好。\n关键操作查找并提升到 MRU 位置:\niterator find(const K& key) {\n auto it = index_.find(key); // 哈希查找\n if (it == index_.end()) return end();\n lru_.erase(lru_.iterator_to(*it)); // 从 LRU 当前位移除\n lru_.push_front(*it); // 推到 MRU\n return iterator(lru_.iterator_to(*it));\n}\n\n淘汰:\nvoid prune(size_t count) {\n for (size_t i = 0; i < count && !lru_.empty(); ++i) {\n auto* node = &lru_.back(); // LRU 尾是最久未用的\n lru_.pop_back();\n index_.erase(index_.iterator_to(*node));\n delete node; // 释放内存\n }\n}\n\n关键限制\n非线程安全:EvictingCacheMap 不提供内部同步,需要外部加锁\n哈希表容量不会自动缩小:setMaxSize() 修改淘汰阈值不会调整哈希桶数\n哈希表初始桶数 = max(capacity/2, 100),之后不会 resize\n\n如何变成并发版本在实际项目中,围绕 EvictingCacheMap 构建并发缓存通常采用分片锁模式:创建 N 个 EvictingCacheMap 实例,根据 key 的哈希值路由到不同的实例,每个实例独立加锁。这种设计思路是工业界最常用的策略。\nvpetrigo/caches — 策略可插拔vpetrigo/caches 是一个轻量级 header-only 库,定位类似 Python 的 cachetools,支持多种淘汰策略。\n特性\n淘汰策略可插拔:LRU、LFU、FIFO 三种策略,通过模板参数切换\n支持自定义哈希表:可将 std::unordered_map 替换为 phmap::node_hash_map 等高性能容器\n线程安全:所有公开方法内部用 std::mutex + RAII 保护\nC++11 兼容:不依赖高版本 C++ 特性\n\n#include \"caches/cache.hpp\"\n#include \"caches/lru_cache_policy.hpp\"\n\n// 定义 LRU Cache 类型\ntemplate <typename K, typename V>\nusing lru_cache_t = typename caches::fixed_sized_cache<K, V, caches::LRUCachePolicy>;\n\nlru_cache_t<std::string, int> cache(256);\ncache.Put(\"hello\", 1);\nauto val = cache.Get(\"hello\"); // 抛出 std::range_error 如果不存在\nauto [val, ok] = cache.TryGet(\"hello\"); // 安全版本,不抛异常\n\n淘汰策略对比\n\n\n策略\nTouch(key) 操作\n淘汰规则\n\n\n\nLRU\n将 key 移到链表头部\n淘汰链表尾部(最久未访问)\n\n\nLFU\n递增 key 的访问计数\n淘汰计数最小的 key\n\n\nFIFO\n无操作\n淘汰最早插入的 key\n\n\n局限性\n无 TTL / 时间过期:纯粹基于容量淘汰,不支持时间维度的过期\n全局锁:单个 std::mutex 保护所有操作,高并发下扩展性有限\nGet 返回引用:锁释放后引用可能悬空(需要调用方立即消费)\n\n适用场景:快速集成、淘汰策略需要灵活切换、并发度不高的项目。\noneTBB concurrent_lru_cache — 工业级并发Intel oneTBB 提供了 concurrent_lru_cache,是目前 C++ 生态中并发级别最高的 LRU Cache 实现之一。它基于 handle 机制和引用计数保证线程安全。\n设计思想\nHandle 访问:operator[] 返回 handle 对象而非裸引用。handle 持有 value 的引用计数,只要 handle 存在,value 就不会被淘汰\n工厂函数:miss 时调用用户提供的工厂函数构造 value,工厂函数需要线程安全\n引用计数淘汰:只有引用计数归零的条目才参与 LRU 淘汰\n预览特性:需要定义 TBB_PREVIEW_CONCURRENT_LRU_CACHE=1\n\n#define TBB_PREVIEW_CONCURRENT_LRU_CACHE 1\n#include <oneapi/tbb/concurrent_lru_cache.h>\n\nstd::string load_from_db(int key) {\n return \"value_\" + std::to_string(key);\n}\n\noneapi::tbb::concurrent_lru_cache<int, std::string> cache(load_from_db, 100);\n\n// handle 持有 value 的引用,保证使用期间不被淘汰\nauto handle = cache[42];\nif (handle) {\n std::cout << handle.value() << std::endl;\n} // handle 析构,引用计数减 1\n\n特点总结\n\n\n维度\n描述\n\n\n\n并发度\n高 —— handle 机制允许真正的并发读\n\n\n淘汰时机\n延迟 —— 只有引用计数归零且容量超限时才淘汰\n\n\n接口风格\n类似工厂模式,miss 时自动构造\n\n\nTTL 支持\n无\n\n\n优点:\n\n真正的并发安全,无需外部加锁\nhandle 解决了「使用中条目被淘汰」的问题\nTBB 质量背书,适合生产环境\n\n缺点:\n\n预览特性,API 可能变化\n依赖 TBB 整个库,引入较重\n淘汰策略只有 LRU,不可切换\n无 TTL\n\nmohaps/lrucache11 — 轻量并发 + TTLlrucache11 是一个 header-only 的 LRU Cache,在经典实现基础上增加了同步控制和 TTL 支持。\n特性\n读写锁:使用 std::shared_mutex,读操作共享锁,写操作独占锁\nTTL 支持:条目可设置过期时间,过期后自动失效\nLoading 模式:miss 时自动调用 loader 函数填充\n支持自定义 Map 类型:可以将底层 unordered_map 替换为其他容器\n\n#include \"LRUCache11.hpp\"\n\nlru11::Cache<std::string, std::string> cache(1000, 0); // 容量 1000,无 TTL\n\n// TTL 模式\ncache.insert(\"key\", \"value\", std::chrono::seconds(60));\n\n// Loading 模式\nauto val = cache.get(\"key\", [](const std::string& k) {\n return load_from_db(k);\n});\n\n优点:\n\n接口类似 Guava LoadingCache,Java 开发者容易上手\nTTL 支持是大多数其他库缺少的功能\nheader-only,零编译依赖\n\n缺点:\n\n读写锁在极高并发下仍有瓶颈\n性能优化不如 folly 或 TBB 深入\n\n分片锁模式(Sharded LRU)这是工业界实际上最常用的并发 LRU 方案——不是设计一个线程安全的 LRU,而是用 N 个独立的 EvictingCacheMap 实例,通过分片分散锁竞争:\ntemplate <typename K, typename V, size_t NumShards = 64>\nclass ShardedLRUCache {\n struct Shard {\n folly::EvictingCacheMap<K, V> map_;\n std::mutex mtx_;\n };\n std::array<Shard, NumShards> shards_;\n\n Shard& getShard(const K& key) {\n size_t h = std::hash<K>{}(key);\n return shards_[h % NumShards];\n }\n\npublic:\n std::optional<V> get(const K& key) {\n auto& shard = getShard(key);\n std::lock_guard lock(shard.mtx_);\n auto it = shard.map_.find(key);\n return it != shard.map_.end() ? std::optional(it->second) : std::nullopt;\n }\n};\n\n这也是 Folly 自身推荐的做法——EvictingCacheMap 不提供内部锁,交由上层灵活组合。分片锁的优点:\n\n锁粒度可控:分片数可以根据并发度自由调整\n实现简单:不需要复杂的无锁数据结构\n热点分散:不同的 key 落在不同的 shard,锁竞争概率降低到 1/NumShards\n可组合:与 EvictingCacheMap、flat_hash_map 等任意容器配合\n\n缺点:不能跨分片做全局 LRU——淘汰只在单个 shard 内进行。在分布均匀的场景下,这通常不成问题。\n进阶:W-TinyLFU(Caffeine 风格)Java 的 Caffeine 缓存库以 W-TinyLFU 策略闻名,在命中率上显著优于纯 LRU。C++ 生态目前没有直接的等价库,但理解其设计模式有助于在需要极致命中率时自研。\nW-TinyLFU 的核心思想纯 LRU 的问题:一次突发流量扫过大量新 key,会把真正的热点数据全部踢出。W-TinyLFU 通过以下机制解决:\n\n三层队列:\n\nWindow LRU (1%):新条目先进入这里,处理突发访问\nProbation LRU (20%):考察区,候选淘汰对象\nProtected LRU (80%):热门区,长期热数据\n\n\nCount-Min Sketch:用极小的内存记录每条 key 的历史访问频率(4 行 × 16K 列 = ~256KB),淘汰时比较候选者的频率,而非简单的 LRU 位序\n\n时间衰减:定期将所有频率计数减半,让「曾经热门、现已冷却」的数据自然降级\n\n\nC++ 中的 Count-Min Sketch 示意class CountMinSketch {\n static constexpr int kDepth = 4;\n static constexpr int kWidth = 1 << 14; // 16K\n std::vector<std::vector<uint32_t>> table_; // 4 × 16K = ~256KB\n uint32_t counter_ = 0;\n\npublic:\n void increment(uint64_t hash) {\n for (int i = 0; i < kDepth; ++i) {\n size_t idx = (hash + i * 0x9e3779b9) & (kWidth - 1);\n table_[i][idx]++;\n }\n // 时间衰减:每 16K 次访问所有计数减半\n if (++counter_ >= kWidth) {\n for (auto& row : table_)\n for (auto& cell : row) cell >>= 1;\n counter_ = 0;\n }\n }\n\n uint32_t estimate(uint64_t hash) const {\n uint32_t min = UINT32_MAX;\n for (int i = 0; i < kDepth; ++i) {\n size_t idx = (hash + i * 0x9e3779b9) & (kWidth - 1);\n min = std::min(min, table_[i][idx]);\n }\n return min; // 取最小值消除哈希冲突导致的过高估计\n }\n};\n\nW-TinyLFU 适合缓存命中率敏感的长期运行服务(如 CDN、广告、推荐系统),但实现复杂度高(~500+ 行核心代码),一般只有在纯 LRU 命中率确实无法满足需求时才值得投入。\n方案对比总结\n\n\n方案\n并发策略\n淘汰策略\nTTL\n依赖\n适用场景\n\n\n\n经典 list+map + shared_mutex\n读写锁\nLRU\n需自建\n无\n小型缓存,快速原型\n\n\nfolly::EvictingCacheMap + 分片锁\n外部 mutex / 分片锁\nLRU\n无\nBoost.Intrusive\n中大型项目,已在用 Folly\n\n\nvpetrigo/caches\n全局 mutex\nLRU/LFU/FIFO\n无\n无(header-only)\n快速集成,需灵活策略\n\n\noneTBB concurrent_lru_cache\nHandle + 引用计数\nLRU\n无\nIntel TBB\n工业级并发,已有 TBB 依赖\n\n\nmohaps/lrucache11\n读写锁\nLRU\n支持\n无(header-only)\n需要 TTL 的 Loading Cache\n\n\n分片锁 + 自选容器\n分片锁\nLRU(shard 内)\n需自建\n取决于容器\n高并发,需要灵活定制\n\n\n自研 W-TinyLFU\n需自建\nW-TinyLFU\n需自建\nCount-Min Sketch\n命中率敏感,大规模长期运行\n\n\n关键要点1. 大部分场景用分片锁模式最实际不是每个项目都有 TBB 或 Folly 依赖。用一个简单的 shared_mutex + 经典 list+map 实现,加上分片扩展,就能覆盖绝大多数需求。分片数选 16-64,锁粒度可控,实现简单。\n2. 注意「全局 LRU」vs「分片 LRU」的区别分片锁模式下,淘汰只在单个 shard 内进行——如果某些 shard 的 key 特别多或特别冷,可能导致整体缓存利用率不均衡。对于 key 分布均匀的场景(大多数场景),影响可忽略。如果对全局 LRU 有硬性要求,需要用 folly 或 TBB 的方案。\n3. TTL 比淘汰策略更重要在实践中,TTL(时间过期)对于缓存正确性的重要性往往超过淘汰策略。数据过期后的刷新比踢掉谁更关键。选择库时优先看 TTL 支持,大多数轻量 C++ LRU 库都不提供 TTL,需要自行封装。\n4. 缓存库选择 = 依赖成本 vs 性能收益\n项目已有 Folly → 直接用 EvictingCacheMap + 分片锁\n项目已有 TBB → 用 concurrent_lru_cache\n零依赖快速集成 → vpetrigo/caches 或 lrucache11\n需要极致性能且可接受复杂度 → 基于 W-TinyLFU 自研\n\n5. C++ 缺少一个「Caffeine 级别」的本地缓存库Java 有 Guava Cache 和 Caffeine,Go 有 groupcache / ristretto,而 C++ 生态目前没有一个集 LRU/LFU/W-TinyLFU 多策略、TTL、权重、统计、异步刷新于一体的成熟本地缓存库。这既是痛点也是机会。\n参考\nfolly::EvictingCacheMap 源码\nvpetrigo/caches\noneTBB concurrent_lru_cache\nmohaps/lrucache11\nCaffeine: W-TinyLFU\n\n","categories":["cpp-devel"],"tags":["c++","folly","lru-cache","并发","tbb"]},{"title":"谈一谈并发队列的实现","url":"/posts/b971a5d0.html","content":"简介本文将谈一谈并发队列,讲解集中可手写的并发队列的实现方式,以及介绍几种开源并发队列的实现,当然都是c++版本的。可手撸的并发队列实现起来相对简单,面试的时候可以撸一撸,简单生产环境也可以用一用;当然,实际生产环境中,还是建议直接用高性能的开源实现。可手撸的版本包括,单锁队列、双锁队列、原子队列;开源并发队列包括boost中的并发队列、tbb中的并发队列、folly中的并发队列、moodycamel中的并发队列。\n单锁队列实现单锁队列是一种实现简单的并发队列,它通过一个锁控制入队和出队,通过两个条件变量分别控制队列空和队列满。数据入队之前判断队列是否满,如果满了,则等待full条件变量;出队前判断队列是否为空,如果为空,则等待empty条件变量。当队列满时出队则唤醒full条件变量;当队列为空并入队时,唤醒empty条件变量。\n代码如下:\n#pragma once\n\n#include <queue>\n#include <vector>\n#include <mutex>\n#include <condition_variable>\n\nclass BlockedQueue {\n public:\n explicit BlockedQueue(size_t capacity) : capacity_(capacity) {}\n\n bool TryPush(int value) {\n std::unique_lock lock(mutex_);\n if (data_.size() >= capacity_) {\n return false;\n }\n Enqueue(value);\n return true;\n }\n\n bool TryPop(int *value) {\n if (value == nullptr) {\n return false;\n }\n std::unique_lock lock(mutex_);\n if (!data_.empty()) {\n return false;\n }\n Dequeue(value);\n return true;\n }\n\n void Push(int value) {\n std::unique_lock lock(mutex_);\n if (data_.size() >= capacity_) {\n full_.wait(lock);\n }\n Enqueue(value);\n }\n\n void Pop(int *value) {\n if (value == nullptr) {\n return;\n }\n std::unique_lock lock(mutex_);\n if (data_.empty()) {\n empty_.wait(lock);\n }\n Dequeue(value);\n }\n private:\n void Enqueue(int value) {\n data_.push(value);\n if (data_.size() == 1) {\n empty_.notify_one();\n }\n }\n\n void Dequeue(int* value) {\n *value = data_.front();\n data_.pop();\n if (data_.size() == capacity_ - 1) {\n full_.notify_one();\n }\n }\n\n private:\n std::mutex mutex_;\n std::condition_variable full_;\n std::condition_variable empty_;\n std::queue<int> data_;\n const size_t capacity_;\n};\n\n上面的实现可以用单链表代替std::queue。这种结构实现简单,临界区较小,可以在请求维度的数据中使用。\n双锁队列实现双锁队列是另一种并发队列,使用两把锁分别控制入队和出队,性能相较于单锁的稍微强点。双锁队列还包括两个条件变量,分别用于表示队列满和队列空;同时还拥有一个原子变量记录当前队列长度。入队列时,先等待“非满”条件变量,然后入队列,如果队列没满,则唤醒“非满”变量。如果入队前队列是空的,入队后就要唤醒“非空”变量。出队时,先等待“非空”条件变量,然后出队,如果队列还有数据,则唤醒“非空”变量。如果出队之前队列时非空的,则出队后唤醒“非满”变量。\n代码如下:\n#pragma once\n\n#include <atomic>\n#include <mutex>\n#include <condition_variable>\n\nstruct Node {\n int val;\n Node* next{nullptr};\n explicit Node(int value = 0): val(value) { }\n};\n\nclass LinkedBlockQueue {\n public:\n LinkedBlockQueue(size_t size): capacity_(size) {\n head_ = new Node;\n tail_ = head_;\n }\n\n bool TryPush(int value) {\n if (count_.load() >= capacity_) {\n return false;\n }\n std::unique_lock lock(push_mutex_);\n Enqueue(value);\n auto current = count_.fetch_add(1);\n if (current + 1 < capacity_) {\n not_full_.notify_one();\n }\n if (current == 0) {\n NotifyNotEmpty();\n }\n\n return true;\n }\n\n bool TryPop(int* value) {\n if (value == nullptr) {\n return false;\n }\n if (count_.load() == 0) {\n return false;\n }\n std::unique_lock lock(pop_mutex_);\n Dequeue(value);\n auto current = count_.fetch_sub(1);\n if (current - 1 == 0) {\n not_empty_.notify_one();\n }\n if (current == capacity_) {\n NotifyNotFull();\n }\n\n return true;\n }\n\n void Push(int value) {\n size_t current = 0;\n {\n std::unique_lock lock(push_mutex_);\n not_full_.wait(lock, [&](){ return count_.load() < capacity_;});\n Enqueue(value);\n current = count_.fetch_add(1);\n if (current - 1 < capacity_) {\n not_full_.notify_one();\n }\n }\n if (current == 0) {\n NotifyNotEmpty();\n }\n }\n\n void Pop(int* value) {\n if (value == nullptr) {\n return;\n }\n size_t current = 0;\n {\n std::unique_lock lock(pop_mutex_);\n not_empty_.wait(lock, [&](){ return count_.load() > 0; });\n Dequeue(value);\n current = count_.fetch_sub(1);\n if (current > 1) {\n not_empty_.notify_one();\n }\n }\n if (current == capacity_) {\n NotifyNotFull();\n }\n }\n\n private:\n void Enqueue(int value) {\n tail_->next = new Node(value);\n tail_ = tail_->next;\n }\n\n void Dequeue(int* value) {\n auto* node = head_->next;\n *value = node->val;\n head_->next = node->next;\n delete node;\n }\n\n void NotifyNotFull() {\n std::unique_lock lock(push_mutex_);\n not_full_.notify_one();\n }\n\n void NotifyNotEmpty() {\n std::unique_lock lock(pop_mutex_);\n not_empty_.notify_one();\n }\n\n private:\n Node* head_;\n Node* tail_;\n std::mutex push_mutex_;\n std::mutex pop_mutex_;\n std::condition_variable not_full_;\n std::condition_variable not_empty_;\n const size_t capacity_;\n std::atomic_size_t count_{0};\n};\n\n代码稍微复杂一点,不过还是可以手工实现的。\n原子队列实现boost中并发队列实现tbb中并发队列实现folly中并发队列实现moodycamel中并发队列实现性能比较","categories":["cpp-devel"],"tags":["c++","boost","folly","tbb","queue","concurrent"]},{"title":"taskflow源码学习及使用","url":"/posts/c1f760b3.html","content":"简介taskflow是一个轻量级的图引擎,或者说是轻量级的tensorflow。taskflow允许用户构建一个有向图,然后并行执行这个图,让图中相互依赖的节点串行执行,不依赖的节点并行执行。这种方式既可视化了业务逻辑的依赖关系,又提升了整体执行的效率。本文将先从源码的角度介绍taskflow库,然后基于taskflow开发一套企业级的策略引擎。\n源码解读taskflow库包括两个部分,图引擎和抢占式线程池。图引擎是taskflow的调度核心;抢占式线程池提升调度的性能。\n抢占式线程池抢占式线程池(work stealing thread pool)是一种多队列线程池,有较好的性能。抢占式线程池包含一个全局队列,线程池中每个线程拥有一个自己的队列。当某个线程执行完自己队列中的任务后,就会从其他队列或者全局队列获取任务进行执行。\n类似线程池还有两种,分别是单队列线程池和多队列线程池。单队列线程池中只有一个全局队列,所有的线程从这个队列中获取任务并进行执行。这种线程池的线程不会出现忙碌不均的情况,但是由于任务来自全局队列,增加的任务竞争,一定程度上影响了性能。多队列线程池中包含多个队列,每个线程拥有一个自己的任务队列。线程池分配接口将任务随机分配给本地队列,然后对应的线程去处理对应任务。这种线程池由于使用本地队列,减少了任务竞争,一定程度上提升了性能,但是本地队列长度不均使得部分线程极其忙碌,部分线程一直空闲,整体资源利用率不高。抢占式线程池的出现,结合了两者有优势,弥补了两者的不足,既提升了性能,又提升了资源利用率。\n接着来讲一下taskflow库中抢占式线程池的实现。它的实现在Executor,其成员变量如下:\n\n_threads 线程组\n_wsq 全局任务队列\n_workers 线程上下文列表,类型是vector\n_wids 线程号与上下文编号对应关系\n_wsq 全局任务队列\n\nExecutor提供了一套run的接口,但是这些接口实际是将任务添加到队列中而已,线程组的线程会从全局队列中获取任务。\nWorker是线程的上下文,成员变量包括:\n\n_id 在_workers中的编号\n_vtm 正在偷取的worker对应的在_workers中的编号\n_rdgen 随机引擎,产生随机数\n_wsq 本地任务队列\n\nExecutor中的线程又是如何配合的呢?? 当Executor执行graph时,会创建一个Topology对象(可以理解为graph的Session),并添加到graph的Topology队列中。如果这个队列只包含一个元素,则将这个Topology中的root节点添加到全局任务队列中(_wsq)。root节点的定义是,这些节点没有依赖节点。添加_wsq之后,唤醒相应数量的线程,这些线程从_wsq取任务。每个节点执行完毕后,会更新该节点的下游节点的状态,如果下游节点没有其他依赖节点,则将该节点添加到本地任务队列中。接着该线程继续执行本地任务队列,直到本地任务队列为空。\n这时,开始偷窃逻辑了。_vtm初始化为_id值,当_vtm等于_id时,从全局队列中获取任务;如果没获得任务,则使用_rdgen随机一个值赋值给_vtm,_vtm对应的Worker对象的本地队列中获取任务;如果依旧没有获取任务,则再次进行随机;重试多次后没有获取任务,并且全局任务队列中有任务,则从全局任务队列中获取;如果还没有任务,先将_vtm还原为_id,然后进行等待。\n本地队列的任务只会由当前线程添加,所以,当本地队列没任务之后,只能从其他队列或者全局队列获取,执行完任务才有可能有新就绪的任务。\nExecutor涉及两个组件,Notifier和TaskQueue。TaskQueue是基于原子指针实现的一写多读循环队列,它使用TaskQueue::Array实现了一个环,然后使用一个指向头的原子变量和一个指向尾的原子变量,控制队列的出队和入队。Notifier是从Eigen拷贝过来的,这里就不讲了。\n图引擎图引擎是taskflow库的核心,用户可以使用Taskflow对象创建一个有向图,然后使用Executor执行。这个有向图执行时,相互依赖的节点串行执行,不相互依赖的节点并行执行。\n图引擎主要涉及Taskflow、FlowBuilder、Topology、Graph、Node、Task类。Node为图中的节点,记录与其他节点的依赖关系;Graph存储Node对象;FlowBuilder负责图构建;Taskflow为顶层图封装;Topology用于基于图的一次执行。\n使用Taskflow创建图时,Taskflow调用父类FlowBuilder的接口,将仿函数装入Node中,并将Node添加到Graph对象里,同时使用Task对象包裹Node类对象的指针。用户使用Task的接口设置Node之间的依赖关系。\n与tensorflow不同,Taskflow的Topology不记录每个节点的执行情况,依赖情况,仅仅记录本次执行的promise和需要等待多少个节点执行,而tensorflow的ExecutorState里记录了每个节点的依赖数量,并实时更新。\n同一个graph多次执行时,每次执行生成一个Topology对象,添加到Taskflow对象的队列中。graph被执行时会从这个队列里取第一个Topology,并初始化所有Node的依赖计数器。每个节点执行完毕,就会更新其下游节点的计数器。如果下游节点已经就绪,则添加任务队列中。\nNode节点包含了大量的结构:\n\n_name 节点名\n_data 数据指针\n_handle 节点类型句柄,包括静态任务、动态任务等等\n_successors 下游节点列表\n_dependents 上有节点列表\n_topology 当前执行的session\n_parent 父图节点\n_state 执行状态\n_join_counter 依赖节点数量\n_semaphores 信号量\n\n图执行时,会将依赖(_dependents)的节点添加就绪队列中;当一个节点执行完毕,则会更新下游节点(_dependents)的依赖节点数量(_join_counter)。如果某个节点的_join_counter为0,表示它依赖的节点都已经执行完毕,则达到就绪状态,可以开始执行了。\n每次取出Topology对象时,都会对每个节点的_join_counter进行初始化,方便后续执行。也由于这个设计,一个图(Taskflow对象)可以并行被调用多次,但是图的每次执行都是串行的,每次被调用就产生一个Topology,只有前一个Topology执行完毕,才会执行后一个Topology。\n为了保障一个图能被并行执行,需要根据这个图创建多个Taskflow对象,每次执行使用不同的对象。这种方式带来的问题,要么每次执行前构建一个图,然后执行;要么创建缓存队列,执行时从队列里取,执行完毕后再放回队列中,如果不够用则动态创建。相对来说,还是后者性能更高一些。当然tensorflow就没有这个问题了,每次执行时创建一个executionStatus就行了。\nNode的仿函数存储在handle_对象中,通过仿函数的参数提取机制,将仿函数装箱到对应的结构中,然后将这个对象塞到variant。调用时判断其类型,设置入参。\n策略引擎参考","categories":["cpp-devel"],"tags":["c++","taskflow","graph engine","strategy server","dag"]},{"title":"使用cmake构建yaf框架","url":"/posts/4d3b3d24.html","content":"简介本文介绍使用cmake构建yaf框架\n背景yafyaf是一个使用c语言实现的php框架, 作为php的拓展加载到php中, 具有较高的性能.php的拓展使用phpize构建, 这类构建工具不友好, 本文将改用更加友好的cmake工具.\ncmakecmake是一款较为友好的构建工具, 其拥有自己的配置语法.cmake工具较为友好的一个方面是, 多种集成开发环境对其支持, 包括:\n\nCLion\ncodeblocks\n等等\n\n比较重要的一点是, CLion使用cmake构建项目, 所以CLion只对能够使用cmake构建的项目友好.这也是phpize不太友好的一点.\n改造cmake将CMakeLists.txt作为项目管理文件, 改造的过程变为编写CMakeLists.txt的过程.\n准备改造之前需要准备的工具包括:\n\n安装cmake\n安装php开发包\n生成config.h\n\n执行yum install cmake命令完成cmake的安装.执行yum install php-devel命令完成php开发包的安装\n使用phpize进行构建过程中, 构架工具会生成一个config.h的文件, 这个文件定义了一些重要的宏.为了降低CMakeLists.txt的编写成本, 本文将通过phpize工具生成config.h, 然后直接使用config.h.\n编写CMakeLists.txtcmake_minimum_required(VERSION 2.8)\nproject(reading_yaf)\n\n# 寻找php-config目录\nif (DEFINED PHP_CONFIG_DIR)\n set(PHP_CONFIG_DIR "${PHP_CONFIG_DIR}/")\nelse ()\n set(PHP_CONFIG_DIR "")\nendif ()\n\n# 读取include目录\nexecute_process(COMMAND ${PHP_CONFIG_DIR}php-config --include-dir\n OUTPUT_VARIABLE PHP_INCLUDE_DIR\n OUTPUT_STRIP_TRAILING_WHITESPACE\n )\n# 读取链接库\nexecute_process(COMMAND ${PHP_CONFIG_DIR}php-config --libs\n OUTPUT_VARIABLE PHP_LIBS\n OUTPUT_STRIP_TRAILING_WHITESPACE\n )\n# 读取链接参数\nexecute_process(COMMAND ${PHP_CONFIG_DIR}php-config --ldflags\n OUTPUT_VARIABLE PHP_LDFLAGS\n OUTPUT_STRIP_TRAILING_WHITESPACE\n )\n# 获取插件存放目录\nexecute_process(COMMAND ${PHP_CONFIG_DIR}php-config --extension-dir\n # RESULT_VARIABLE PHP_EXTDIR\n OUTPUT_VARIABLE PHP_EXTDIR\n OUTPUT_STRIP_TRAILING_WHITESPACE\n )\n\n# 添加宏\nadd_definitions(\n -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1\n -DHAVE_CONFIG_H\n -DPHP_ATOM_INC\n)\n# 包含目录, 保持与php-config --includes的结果一致\ninclude_directories(\n BEFORE\n ${PHP_INCLUDE_DIR}\n ${PHP_INCLUDE_DIR}/Zend\n ${PHP_INCLUDE_DIR}/main\n ${PHP_INCLUDE_DIR}/TSRM\n ${PHP_INCLUDE_DIR}/ext\n ${PHP_INCLUDE_DIR}/ext/date/lib\n)\n\n\n# 添加include目录\ninclude_directories(.)\ninclude_directories(configs)\ninclude_directories(requests)\ninclude_directories(responses)\ninclude_directories(routes)\ninclude_directories(views)\n\n# 获取源文件\naux_source_directory(. SRC)\naux_source_directory(configs SRC)\naux_source_directory(requests SRC)\naux_source_directory(responses SRC)\naux_source_directory(routes SRC)\naux_source_directory(views SRC)\n\n# 添加构建目标\nadd_library(yaf SHARED ${SRC})\n\n# 添加安装目录\ninstall(\n TARGETS yaf\n LIBRARY DESTINATION ${PHP_EXTDIR}\n)\n\n# 设置构建产物的命名, 包括\n# - 去掉"lib"前缀\n# - 统一后缀为".so"\nset_target_properties(\n yaf PROPERTIES\n PREFIX ""\n SUFFIX ".so"\n)\n\n\n参考","categories":["essay"],"tags":["php-devel","php","cmake","yaf"]},{"title":"使用 Claude Code 撰写博客","url":"/posts/8bb7772d.html","content":"简介Claude Code 是 Anthropic 推出的命令行 AI 编程助手,除了写代码,它也能高效地撰写技术博客。本文将介绍如何用 Claude Code 在 Hexo 博客中完成从草稿到发布的全流程。\n为什么用 Claude Code 写博客传统的博客写作流程是:打开编辑器、查资料、写内容、调格式、构建发布。Claude Code 可以把这个流程高度自动化:\n\n直接操作文件:Claude Code 可以读取、编辑、创建 Markdown 文件,无需手动复制粘贴\n上下文感知:它能理解你的项目结构(Hexo、NexT 主题),自动生成符合格式的 front matter\n内容生成:给定主题,它能组织文章结构并生成完整内容\n格式规范:自动遵循 Hexo 的 Markdown 格式要求,标签、分类、描述一应俱全\n\n准备工作环境要求\n安装 Claude Code(通过 npm install -g @anthropic-ai/claude-code 或直接使用 CLI 版本)\n一个 Hexo 博客项目(本文以 hexo + NexT 主题为例)\n建议在项目根目录配置 CLAUDE.md,让 Claude Code 了解项目结构\n\n配置 CLAUDE.md在博客项目根目录创建 CLAUDE.md,内容如下:\n# 简介\n基于 hexo 和 NexT 主题打造的个人博客网站。\n\n# 构建\n- 清理 yarn run clean\n- 生成页面 yarn run gen\n- 启动调试服务 yarn run gen\n\n# 重要库版本信息\n- hexo 8.1.2\n- hexo-theme-next 8.27.0\n\n# 目录结构\n- source/_posts 博客源文目录\n\n配置好后,Claude Code 会自动读取这个文件,了解项目结构和构建命令。\n写博客的三种方式方式一:从零生成直接告诉 Claude Code 你的需求,它会自动创建文件并写入内容:\n在 source/_posts/others/ 下创建一篇hexo博文,主题是"Git 工作流最佳实践",包含分支策略、commit 规范、PR 流程等小节\n\nClaude Code 会自动:\n\n在指定目录创建 .md 文件\n填写正确的 YAML front matter(title、categories、tags、date 等)\n生成结构化的文章内容\n\n方式二:填充草稿先创建带 front matter 的空白文件,再让 Claude Code 填充内容:\n\n手动创建文件骨架:\n---\ntitle: 你的标题\ncategories: essay\ndate: 2026-05-24 23:36:52\ntags: [tag1, tag2]\n---\n\n告诉 Claude Code:\n在 source/_posts/xxx.md 中写一篇关于XXX的博文\n\n方式三:与已有文章协作在已有文章基础上修改或扩展:\n\n续写:在已写好的内容后面继续写\n润色:让 Claude Code 优化表达、修正错别字\n翻译:中英文互译\n\n实际操作步骤步骤 1:启动 Claude Code在博客项目根目录下启动:\ncd /path/to/your/blog\nclaude\n\n步骤 2:描述需求清晰描述你的写作需求,关键要素包括:\n\n主题:文章要讲什么\n目录:放在 source/_posts/ 下的哪个子目录\n风格:技术教程、随笔、翻译等\n小节:希望包含哪些内容模块\n\n示例提示词:\n在 source/_posts/others/ 下写一篇hexo格式的博文,主题是"Docker 网络原理"。\n分四个小节:网络模式概述、bridge模式详解、host模式详解、容器间通信实战。\n每个技术点配合代码示例。标签基于内容自动生成。\n\n步骤 3:预览和调整生成后,启动 Hexo 本地服务预览效果:\nyarn run gen\n\n浏览器访问 http://localhost:4000 查看渲染效果,不满意的地方直接告诉 Claude Code 修改。\n步骤 4:发布确认无误后,提交代码并部署:\ngit add source/_posts/xxx.md\ngit commit -m \"add: 新博文\"\nhexo deploy\n\n实用技巧添加代码高亮在 Hexo 中,用三个反引号包裹代码块并指定语言即可自动高亮。Claude Code 生成的代码块默认就是这个格式,无需额外处理。\n文章配图如果需要配图,告诉 Claude Code 图片应该出现在什么位置,它会插入占位符或描述,之后手动替换为实际图片路径。\n多人协作Claude Code 支持以 -p 参数传入单次指令的模式,适合放入脚本批量生成文章草稿:\nclaude -p \"在 source/_posts/others/draft.md 中写一篇关于XXX的大纲\"\n\n自定义写作风格在 CLAUDE.md 中可以声明写作偏好:\n# 写作规范\n- 使用中文写作\n- 技术名词保留英文原文\n- 代码示例需要带注释\n- 每个小节字数控制在 500 字以内\n\n局限和注意事项\nAI 生成的技术内容需要人工审核,确保准确性和时效性\n个人观点类文章建议自己撰写,Claude Code 更适合教程、参考类内容\n复杂的文章建议分多次交互完成,每次聚焦一个小节\n生成的代码示例建议在本地验证后再发布\n\n总结Claude Code 将博客写作从”打开编辑器慢慢写”变成了”对话式写作”,特别适合技术博客这类结构化内容。核心流程就是:描述需求 → 生成内容 → 预览调整 → 发布,大幅降低了写博客的心理门槛和时间成本。\n","categories":["essay"],"tags":["claude-code","hexo","博客","AI写作"]},{"title":"graphviz_learn","url":"/posts/3a17fc38.html","content":"参考: graphviz教程\n","categories":["essay"]},{"title":"基于CLion远程开发","url":"/posts/fa9b04c2.html","content":"简介本文介绍基于docker和projector搭建一套c++的开发环境,提升研发效率问题。大多数c++服务是运行在linux平台的,代码也只能在linux上编译,但大部分开发者使用的是macbook或者thinkpad,很少用linux开发。开发环境、编译运行环境两边代码同步影响研发效率。隔壁的vscode通过remote插件,支持远程开发模式,一定程度上提升了研发效率。年初Jetbrains开源的projector_install项目,旨在让IDE运行在服务端,然后本地通过网络与其交互。本文将介绍这个环境的搭建。\n环境搭建有vps或者linux开发机的同学,可以参考dockerfile进行安装。没有vps或者linux开发机的同学,可以在macbook或者thinkpad上安装一个docker,然后使用下面dockerfile进行安装。Dockerfile内容如下:\nFROM centos:8\n\n# 基本开发环境\nRUN dnf install -y gcc-c++ cmake make git sudo; \\\n dnf clean all; \\\n rm -rf /var/cache/dnf/*\n\n# python环境\nRUN dnf install -y python3 python3-pip python3-pyOpenSSL python3-cryptography \\\n less libXext libXrender libXtst libXi freetype; \\\n dnf clean all; \\\n rm -rf /var/cache/dnf/*\n\n# 创建用户及目录\nRUN useradd -b /home -m -s /bin/bash work\nWORKDIR /home/work\nRUN usermod -a -G wheel work\nRUN echo "work:work"|chpasswd \nUSER work\n\n# 安装projector和conan\n## 更新pip\nRUN python3 -m pip install -U pip --user; pip3 cache purge;\n## 安装projector_install和conan\nRUN pip3 install projector-installer conan --user; pip3 cache purge;\n## 手工初始化目录,否则启动的时候需要协议确认\nRUN mkdir -p .projector/apps .projector/cache .projector/configs\n## 安装IDE\nRUN .local/bin/projector autoinstall --config-name CLion --ide-name "CLion 2021.2.3" --port 9999; \\\n rm .projector/cache/*\n\n# 暴露端口号\nEXPOSE 9999\n\nENTRYPOINT [ "/home/work/.local/bin/projector", "run", "CLion" ]\n\n然后执行下面命令进行镜像构建及推送:\ndocker build -t phantom9999/projector:v1 .\ndocker push phantom9999/projector:v1\n最后执行下面的命令启动容器:\ndocker run -d -p 9999:9999 phantom9999/projector:v1\n\n懒得自己构建镜像的同学,可以直接从docker hub上拉对应的镜像(虽然有点大):\ndocker pull phantom9999/projector:v1\n\nJetbrains也提供了对应的镜像(系统是ubuntu的),可以在docker hub上找到。\n环境使用在jetbrains的网站上找到projector工具,下载native app,然后连接上云端的CLion即可。\nprojector貌似是专门优化的windows,在windows上操作无误,但是在mac系统上,稍稍有点小问题。\n参考本文使用的代码及命令可以在github的代码库上找到。\n","categories":["essay"],"tags":["Jetbrains","CLion","docker","projector"]},{"title":"Hexo博客搭建及优化","url":"/posts/121dda.html","content":"简介本文将介绍基于hexo博客搭建的过程, 包括, 博客搭建, 主题选择, 插件安装以及插件使用等.\n博客搭建hexo博客是纯静态的博客, 用户通过编写markdown文件完成博客编写, 然后使用hexo提供的构建工具将md文件生成静态页面.hexo使用主题模板将md渲染成静态html页面. 最后, 用户将这些静态页面部署到服务器中, 完成整个博客的部署.\n主题安装建议主题:\n\nnext 简约好看\n\n插件安装推荐插件:\n\nhexo-abbrlink 生成固定链接, 降低url长度\nhexo-all-minifier 压缩html/css/js, js打包到一个文件的功能有问题.\nhexo-deployer-git 自动化部署\nhexo-filter-flowchart markdown支持流程图\nhexo-filter-optimize 将js打包成一个文件, 将main.css内联到html中\nhexo-filter-plantuml markdown支持uml\nhexo-filter-sequence markdown支持时序图\nhexo-generator-baidu-sitemap 生成百度的sitemap\nhexo-generator-search 支持即时搜索\nhexo-generator-sitemap 生成sitemap\nhexo-offline 缓存支持\n\n","categories":["essay"],"tags":["hexo","优化","博客搭建","github pages"]},{"title":"unite.vim插件使用及配置","url":"/posts/b0d02bc6.html","content":"简介本文主要介绍unite.vim的使用与配置.unite.vim是Shougo开发的一款插件, 确切的说, unite就像vim插件里面的vim.这个插件功能繁多, 配置项目较多, 一定程度上怎么了用户的学习成本, 本文将详细介绍这个工具的使用与配置.\n功能介绍本文介绍的unite.vim的功能包括unite.vim集成的功能以及第三方插件的功能.unite.vim由于其丰富的功能以及高扩展性, 已经衍生出了多款基于unite.vim的插件.\nunite.vim集成的功能包括:\n\nbookmark 书签功能\nbuffer, buffer_tab buffer浏览功能\nchange 列举变动\ncommand ex命令\ndirectory, directory/new, directory_mru 目录相关功能\nfie, file/new, file_point, file_rec 文件相关功能\nfunction 函数相关功能\ngrep\nhistory\njump, jump_point 跳转相关功能\nlauncher\nline, line/fast 行号相关功能\nmapping\nmenu\nneocomplete\noutput\nprocess\nresume\nruntimepath\nsource\ntab\nvimgrep\nwindow\n\n基于unite.vim的插件包括:\n\nunite-help\nunite-tag\nunite-outline\nunite-colorscheme\nunite-font\nunite-locate\nunite-everything\nunite-mark\nunite-alias\nunite-script\nunite-git_grep\nunite-remotefile\nunite-neco\nunite-rake\nunite-history\nunite-qflist\nunite-gem\nunite-qf\nunite-session\nunite-svn\nunite-rails\nunite-grails\nunite-cake\nunite-zf, unite-sf2\nunite-ack\nunite-launch\nunite-transparency\nquicklearn\nvim_hacks\nhaskellimport\nunite-equery\nunite-file-vcs\nunite-radio.vim\nunite-gist\nvim-unite-id\nunite-ref\n\nbuffer功能介绍与配置vim中的buffer类似于ide中已经打开的文件, vim将已经打开的文件保存到buffer中, 方便用户去使用.unite.vim内置buffer功能, 使用:Unite source命令可以看到.unite.vim提供的buffer功能包括:\n\nbuffer\nbuffer_tab\n\n其中执行:Unite buffer命令后, 会在新的tab中显示buffer列表, 而执行:Unite buffer_tab后会在当前tab中显示buffer.unite为buffer选择提供了即时搜索的功能, 用户可以搜索关键词, 然后unite查找buffer对应的文件, 然后进行排序. unite查找的内容仅限于路径和文件名.\nvim中有类似功能的插件包括, MinBufExplorer和bufexporer插件.\nbufexporer插件使用简单, 它提供三个命令分别是\\be(打开历史文件列表), \\bv(水平创建一个tab显示buffer信息), \\bs(垂直创建一个tab显示buffer信息).这个插件不需要配置, 加载即可使用. 比较麻烦的是, 快捷键比较逆天, 而且不支持buffer的搜索.\nMinBufExporer会开一个狭小的tab显示buffer列表信息.使用minBufExporer方面, minbufexporer跟bufexporer一样, 不需要配置, 可以直接使用.在minBufExporer使用:bn(下一个buffer), :np(上一个buf), :b"num",:MiniBufExporer(打开tab, 并显示buffer信息), :CMiniBufExporer(关闭buffer的tab).\n与这两个插件相比, unite buffer显得无比强大好用.\n文件查找功能介绍与配置outline功能介绍与配置","categories":["essay"],"tags":["vim","unite.vim","vim插件","vim配置"]},{"title":"yaf源码阅读","url":"/posts/c1026c5e.html","content":"简介本文将分别从接口层和实现层解读yaf框架.\n接口层介绍yaf是一个使用c实现的高性能框架. 它以php拓展的形式实现整个php框架.\n它的优点包括:\n\n用C语言开发的PHP框架, 相比原生的PHP, 几乎不会带来额外的性能开销.\n所有的框架类, 不需要编译, 在PHP启动的时候加载, 并常驻内存.\n更短的内存周转周期, 提高内存利用率, 降低内存占用率.\n灵巧的自动加载. 支持全局和局部两种加载规则, 方便类库共享.\n高性能的视图引擎.\n高度灵活可扩展的框架, 支持自定义视图引擎, 支持插件, 支持自定义路由等等.\n内建多种路由, 可以兼容目前常见的各种路由协议.\n强大而又高度灵活的配置文件支持. 并支持缓存配置文件, 避免复杂的配置结构带来的性能损失.\n在框架本身,对危险的操作习惯做了禁止.\n更快的执行速度, 更少的内存占用.\n\n框架执行整个框架分为应用程序组件集和其他基础组件集. 在应用程序组件集中包含了, 插件, 分发器, 路由器, 控制器, 启动器等. 基础组件包括, session组件, 注册表组件, 自动加载组件, 配置组件.\nyaf框架包含多个层次, 分别是:\n\nApplication 应用程序, 一个服务作为一个应用程序\nModule 模块, 一个服务包含多个模块\nController 控制器, 一个模块包含多个控制器\nAction 动作, 一个控制器包含多个动作\n\nyaf是传统的php框架, 使用传统的执行流程. 这类执行流程无法上下文复用, 每处理一个请求就要初始化一次框架.\n处理用户请求时, yaf框架先创建应用程序(Application类对象), 应用程序类创建启动器(Bootstrap类对象), 启动器执行分发器加载, 插件加载等操作. 然后应用程序将请求封装成请求对象(Request类对象), 将请求对象交付给分发器, 分发器根据路由器中的路由规则, 获得对应的处理对象. 路由器根据URI信息, 查询路由规则, 找到匹配的路由规则, 根据路由规则获得相应的处理器(Controller类对象), 并将其返回给分发器. 分发器执行处理器, 将处理结果封装成相应对象(Response对象)返回给应用程序. 应用程序将响应对象返回给用户. 框架的执行流程如图所示:\n\n自动加载组件yaf框架为了兼容老版本的php, 提供两种自动加载的策略, 分别是基于下划线的自动加载方案和名字空间加载方案. yaf的加载方案在配置文件中指定.\nyaf中库的分为本地库和全局库, 全局库作用于整个应用程序, 本地库只作用于当前模块(Module), 当当前应用程序中只包含一个模块时, 全局库和本地库的区别只是位置的不同罢了. \n下划线自动加载方案这种自动加载方案会对需要加载的类的类名进行处理, 将类名中的下划线(_)替换为替换为斜线(/), 生成类路径, 然后进行库文件查找. 库查找时, 先获得本地库的路径, 接着融合本地库路径和类目录, 得到类文件的绝对路径, 然后判断这个文件是否存在, 如果存在, 则进行加载, 否则进行全局库查找.\n执行的流程如下:\n\n\n\n\n\n\n\n\n路由组件分发器组件插件机制从图中可以看到, 整个框架的核心是这个分发器. yaf框架针对分发的执行流程, 在各个执行片段添加了钩子, 并通过这些钩子制作了插件机制.\nyaf的钩子包括:\n\nrouterStartup 在查询路由规则之前执行\nrouterShutdown 在查询完路由规则之前执行\ndispatchLoopStart 在分发流程开始前执行\npreDispatch 在分发操作之前执行\npostDispatch 在分发操作完毕执行\ndispatchLoopShutdown 在分发流程结束后执行\n\n实现层参考","categories":["essay"],"tags":["php","yaf"]},{"title":"环境部署","url":"/posts/8c50c265.html","content":"","categories":["essay"]},{"title":"advanced-usage","url":"/posts/b4c73ab2.html","content":"高级主题深度解析本文档对 NV Embedding Cache SDK 中的高级主题进行深度分析,涵盖执行上下文、缓存管理、插入启发式策略、多设备部署、线程池与分配器、AOTInductor C++ 部署等内容。每个主题先介绍概念,再结合源码分析实现细节。\n\n一、执行上下文(ExecutionContext)概念执行上下文(ExecutionContext)是 NVE 中管理 GPU 操作所需状态和资源的容器。每个上下文代表一个可重用的并行执行环境——持有独立的 CUDA 流对(lookup stream + modify stream)、临时缓冲区、线程池和分配器引用。应用应创建固定数量的上下文,在初始化时完成分配,然后在运行时以轮询等方式循环使用。\n类层次ExecutionContext (基类, include/execution_context.hpp)\n ├── GPUEmbeddingTableExecutionContext (GPUEmbeddingLayer 专用)\n ├── GPUTableExecutionContext (GpuTable 专用)\n └── HostTableExecutionContext (HostTable 专用)\n\n核心成员class ExecutionContext {\nprotected:\n cudaStream_t lookup_stream_; // 查找操作流\n cudaStream_t modify_stream_; // 修改操作流\n thread_pool_ptr_t thread_pool_; // CPU 线程池引用\n allocator_ptr_t allocator_; // 内存分配器引用\n std::unordered_map<std::string, std::shared_ptr<ResizeableBuffer>> buffer_storage_;\n // 按名称索引的临时缓冲区池\n std::unordered_map<std::string, std::vector<cudaStream_t>> aux_streams_storage_;\n // 辅助 CUDA 流\n};\n\n关键实现细节get_buffer() — 懒分配临时缓冲区\nvoid* ExecutionContext::get_buffer(const std::string& name, size_t size, bool host_alloc) {\n auto internal_name = internal_name(name, host_alloc);\n auto it = buffer_storage_.find(internal_name);\n if (it == buffer_storage_.end()) {\n // 按需分配,下次同名的 get_buffer 直接复用\n auto buf = std::make_shared<ResizeableBuffer>(size, host_alloc, allocator_);\n buffer_storage_.emplace(internal_name, buf);\n return buf->data();\n }\n // 已存在则确保容量足够\n it->second->reserve(size);\n return it->second->data();\n}\n\nResizeableBuffer 按需增长,但不会自动缩小。因此首次用到某个大尺寸操作时会触发分配,后续同尺寸操作复用已分配内存。\nget_aux_streams() — 辅助流\nstd::vector<cudaStream_t> ExecutionContext::get_aux_streams(\n const std::string& name, size_t num_streams) {\n auto it = aux_streams_storage_.find(name);\n if (it == aux_streams_storage_.end()) {\n std::vector<cudaStream_t> streams(num_streams);\n for (auto& s : streams) cudaStreamCreate(&s);\n aux_streams_storage_.emplace(name, std::move(streams));\n }\n return it->second;\n}\n\nwait() — 同步全部工作\nvirtual void wait() {\n cudaStreamSynchronize(lookup_stream_);\n cudaStreamSynchronize(modify_stream_);\n for (auto& kv : aux_streams_storage_) {\n for (auto& stream : kv.second) {\n cudaStreamSynchronize(stream);\n }\n }\n}\n\n使用模式// 初始化阶段创建固定数量的上下文\nstd::vector<context_ptr_t> contexts;\nfor (int i = 0; i < num_threads; i++) {\n cudaStream_t ls, ms;\n cudaStreamCreate(&ls);\n cudaStreamCreate(&ms);\n contexts.push_back(layer->create_execution_context(ls, ms, thread_pool, allocator));\n}\n\n// 运行时循环使用\nint ctx_idx = 0;\nfor (auto& batch : batches) {\n auto ctx = contexts[ctx_idx++ % contexts.size()];\n layer->lookup(ctx, batch.num_keys, batch.keys, batch.output, ...);\n}\n\n// 销毁前必须先释放所有上下文\ncontexts.clear();\nlayer.reset();\n\n\n二、缓存管理Modify 操作与 ModifyContext所有改变缓存内容或驻留状态的操作(insert / update / update_accumulate / erase / clear)统称为 Modify 操作。每个 Modify 操作需要一个 ModifyContext——由 EmbedCacheSA 在 modify_context_create() 时分配,持有批量操作所需的临时缓冲区。\n约束:GPU 端不支持多个 Modify 操作同时进行。用户必须确保 Modify 操作的串行化(例如通过单个 CUDA 流启动所有 Modify 操作)。NVE 的 Layer 层在内部通过 kernel_launch_mutex_ + modify_in_progress_ event + private_modify_stream_ 实现了这个约束。\nInvalidate and Commit 范式为了支持 Lookup 和 Modify 操作并行执行,NVE 采用”失效并提交”范式:\n阶段 1 — Invalidate(失效):\n Modify 操作首先启动 CUDA kernel 将相关 cache 条目标记为"失效中"\n → 后续 Lookup 对这些条目的访问会 miss(转到后备表读取)\n ↓\n阶段 2 — Wait(等待):\n 等待所有正在进行中的 Lookup 操作完成\n → 通过 cudaEvent + cudaStreamWaitEvent 实现异步等待\n → 通过 ContextRegistry 收集所有活跃的 lookup stream\n ↓\n阶段 3 — Commit(提交):\n 安全地修改缓存内容(写新数据、更新 tag)\n 重新启用失效的条目\n → 后续 Lookup 可以命中新写入的数据\n\nCustom Flow API对于使用多个 CUDA kernel 实现自定义查找流程的用户,Invalidate and Commit 需要额外的协调。如果用户使用超过一个 kernel 的流水线实现查找,需要调用 start_custom_flow() / end_custom_flow() API 来维持所需的原子性视图。\n流同步细节GPUTable 中 update/accumulate 操作的同步序列:\n// Step 1: 等待所有进行中的查找完成\nsync = contexts_->create_sync_event();\nsync->event_record();\nsync->event_wait_stream(update_stream);\n// 此时所有 lookup stream 上的 work 已完成\n\n// Step 2: 执行修改\nUpdateTable<KeyType>(... , update_stream);\n\n// Step 3: 标记完成\ncudaEventRecord(uvm_update_event, update_stream);\ncudaStreamWaitEvent(modify_stream, uvm_update_event);\n\n\n三、插入启发式策略(InsertHeuristic)接口// include/insert_heuristic.hpp\nclass InsertHeuristic {\npublic:\n virtual ~InsertHeuristic() = default;\n virtual bool insert_needed(const float hitrate, const size_t table_id) = 0;\n};\n\nhitrate 是当前查找操作的命中率(范围 0.0 ~ 1.0),table_id 是层级中表的索引。返回值决定是否触发一次自动插入。\nDefaultInsertHeuristic默认插入策略,使用阈值比较:\nclass DefaultInsertHeuristic : public InsertHeuristic {\npublic:\n static constexpr float DEFAULT_THRESHOLD = 0.75f;\n\n DefaultInsertHeuristic(const std::vector<float> thresholds) : thresholds_(thresholds) {}\n\n bool insert_needed(const float hitrate, const size_t table_id) override {\n if (hitrate >= thresholds_.at(table_id)) {\n return false; // 命中率足够高,不需要插入\n }\n // 加入随机抖动,避免多表同步触发插入\n return dis_(gen_) < (thresholds_.at(table_id) - hitrate);\n }\n};\n\n关键细节:\n\n随机抖动:dis_(gen_) < (threshold - hitrate) 使得插入概率正比于命中率缺口。命中率远低于阈值时几乎必然触发插入,略低于阈值时概率较低。这避免了所有表同时触发插入导致的性能毛刺\n层级默认阈值:对于 HierarchicalEmbeddingLayer,最后一张 host 表的阈值设为 0.0(永不触发自动插入,因为该层已有全量数据)\n\nNeverInsertHeuristicclass NeverInsertHeuristic : public InsertHeuristic {\npublic:\n bool insert_needed(const float, const size_t) override { return false; }\n};\n\n完全禁用自动插入。插入操作仅通过用户显式调用 layer->insert() 发生。\nFSMInsertHeuristic有限状态机驱动的插入策略,维护每张表的 prev_hitrate_ 和 state_:\nStates: Start → Steady\n\n逻辑(从命名推断):\n Start: 初始状态,连续低命中率时触发插入\n Steady: 稳定状态,根据命中率变化趋势决定是否插入\n\n目前标注为 // TODO: TRTREC-88,可能尚未完成。\nStatisticalInsertHeuristic基于统计概率的插入策略,使用 Chebyshev 不等式判断命中率是否显著偏离期望:\nclass StatisticalInsertHeuristic : public InsertHeuristic {\n // 维护每个表的命中率滑动窗口(默认 14 个样本)\n std::vector<std::deque<float>> hitrate_window_;\n std::vector<float> hitrate_mean_;\n std::vector<float> hitrate_var_;\n\n // 使用切比雪夫不等式计算概率\n float getChebyshevProb(const size_t table_id);\n\n // 状态机: Start → Insert → Steady → Unstable\n enum class State { Start, Insert, Steady, Unstable };\n};\n\n工作机制:\n\n在每个查找操作后更新命中率的滑动窗口均值 mean 和方差 var\n使用切比雪夫不等式计算当前命中率偏离均值的概率上限:P(|X - μ| ≥ kσ) ≤ 1/k²\n如果概率超过阈值且偏离方向为负(命中率下降),触发插入\n状态机管理插入后的稳定期,避免频繁触发\n\n配置参数:\n\n\n\n参数\n默认值\n说明\n\n\n\nk_factor\n2.6\n标准差倍数因子。越大越不敏感,越小越容易触发插入\n\n\nwindow_size\n14\n滑动窗口大小,用于计算均值和方差\n\n\nnum_inserts_needed\n50\n需要连续插入多少次才认为状态稳定\n\n\nmax_unsteady_samples\n3\n允许的最大连续不稳定样本数\n\n\n\n四、多设备与分布式支持多 GPU 共享线性表当嵌入表跨多块 GPU 时,有两种方案:\n方案 1:UVM 共享表(单节点多 GPU)\n使用 cudaMallocManaged 分配 UVM 内存,通过 cudaMemAdvise 优化访问:\nvoid* linear_table = nullptr;\ncudaMallocManaged(&linear_table, linear_table_size);\n\n// 建议 GPU 0 频繁访问此区域\ncudaMemAdvise(linear_table, linear_table_size,\n cudaMemAdviseSetAccessedBy, 0);\n// 将首选位置设为 CPU,保证所有 GPU 公平访问\ncudaMemAdvise(linear_table, linear_table_size,\n cudaMemAdviseSetPreferredLocation, cudaCpuDeviceId);\n\n每个 GPU 创建独立的 GpuTable,共享同一个 uvm_table 指针:\nauto gpu_tab1 = std::make_shared<GpuTable<int64_t>>(cfg); // device 0\ncfg.device_id = 1;\nauto gpu_tab2 = std::make_shared<GpuTable<int64_t>>(cfg); // device 1\n\n// 独立 GPU cache,共享 UVM 后备表\n\nUVM 更新冲突通过 disable_uvm_update 控制——只需一个 GpuTable 负责 UVM 写入,其余只读。\nCUDADistributedBuffer(多节点)CUDADistributedBuffer(include/distributed.hpp)在多节点场景下创建跨设备共享的 CUDA 虚拟地址映射缓冲区:\nclass CUDADistributedBuffer {\n std::byte* ptr(); // 本地映射的缓冲区指针\n uint64_t shard_size(); // 每个分片的大小\n uint64_t num_shards(); // 总分片数 = world_size\n};\n\n初始化流程:\ninit_single_host() — 单节点多 GPU:\n 1. 每个 GPU 分配物理内存\n 2. 通过 CUDA Virtual Memory Management API\n (cuMemCreate → cuMemMap → cuMemSetAccess)\n 将所有分片映射到所有 GPU 的虚拟地址空间\n 3. 每个 rank 通过 all_gather 交换分配句柄\n\ninit_multi_host() — 多节点:\n 1. 每节点分配本地 GPU 内存\n 2. 通过 DistributedEnv(MPI)交换句柄\n 3. 使用 NVLink/NVSwitch 或网络实现跨节点访问\n\nDistributedEnv抽象了分布式环境接口:\n\n\n\n方法\n作用\n\n\n\nrank()\n当前进程编号\n\n\nworld_size()\n总进程数\n\n\ndevice_count()\n每节点的 GPU 数\n\n\nlocal_device()\n当前进程的本地 GPU 编号\n\n\nsingle_host()\n是否单节点\n\n\nbarrier()\n同步屏障\n\n\nbroadcast()\n广播\n\n\nall_gather()\n全收集\n\n\nPython 端通过 pynve.nve.MPIMemBlock 封装,使用 mpi4py 进行进程间通信。\n实现细节:CUDA Virtual MemoryCUDADistributedBuffer::init_single_host() 使用 CUDA Virtual Memory Management API:\n// 1. 创建物理内存块\nCUmemAllocationProp prop = {};\nprop.type = CU_MEM_ALLOCATION_TYPE_PINNED;\nprop.location.type = CU_MEM_LOCATION_TYPE_DEVICE;\nprop.location.id = device_id;\ncuMemCreate(&alloc_handle_, shard_size_, &prop, 0);\n\n// 2. 映射到虚拟地址空间\ncuMemAddressReserve(&ptr_, total_size_, 0, 0, 0);\ncuMemMap(ptr_ + i * shard_size_, shard_size_, 0, alloc_handle_, 0);\n\n// 3. 设置访问权限\nCUmemAccessDesc access = {};\naccess.location.type = CU_MEM_LOCATION_TYPE_DEVICE;\naccess.location.id = target_device_id;\naccess.flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;\ncuMemSetAccess(ptr_ + i * shard_size_, shard_size_, &access, 1);\n\n这种方式实现的是真正的跨 GPU 零拷贝共享——每块 GPU 通过自己的物理内存访问远端 GPU 的缓冲区,由 NVLink/NVSwitch 提供硬件转发。\n\n五、线程池(ThreadPool)接口// include/thread_pool_base.hpp\nclass ThreadPool {\npublic:\n using task_type = std::function<void()>;\n using result_type = std::future<void>;\n\n virtual result_type submit(task_type task) = 0; // 提交单个任务\n virtual void execute_n(int64_t workgroup, int64_t num_tasks,\n const std::function<void(int64_t)>& task) = 0; // 批量并行\n virtual int64_t num_workers() const = 0;\n};\n\nSimpleThreadPoolSimpleThreadPool(src/thread_pool.cpp)是默认实现:\n\n创建时启动固定数量的 worker 线程\nsubmit() 将任务加入工作队列,线程池竞争获取\nexecute_n() 批量提交索引化任务并等待全部完成,通过 std::promise/std::future 同步\n\n核心用途:CPU 端 HostTable 的并行查找、默认嵌入填充、CPU UVM accumulate。\n使用模式thread_pool_ptr_t pool = create_thread_pool({{\"num_workers\", 16}});\n\nconst int64_t num_tasks = (n + chunk_size - 1) / chunk_size;\npool->execute_n(0, num_tasks, [&](int64_t idx) {\n int64_t start = idx * chunk_size;\n int64_t end = std::min(start + chunk_size, n);\n for (int64_t i = start; i < end; i++) {\n process(i);\n }\n});\n// execute_n 在此处阻塞直到所有任务完成\n\n\n六、分配器(Allocator)接口class Allocator {\npublic:\n virtual cudaError_t device_allocate(void** ptr, size_t sz, int device_id = -1) noexcept = 0;\n virtual cudaError_t device_free(void* ptr, int device_id = -1) noexcept = 0;\n virtual cudaError_t host_allocate(void** ptr, size_t sz) noexcept = 0;\n virtual cudaError_t host_free(void* ptr) noexcept = 0;\n virtual cudaError_t set_device(int device_id) noexcept = 0;\n};\n\nDefaultAllocatorDefaultAllocator 使用 cudaMalloc / cudaFree 分配 GPU 内存,使用 cudaMallocHost / cudaFreeHost 分配主机固定内存(page-locked memory,支持 GPU 直接访问)。\n大页支持:对于大块主机内存分配(≥2MB),DefaultAllocator 会尝试使用 mmap 分配透明大页(Transparent Huge Pages),减少 TLB miss:\nsize_t get_largest_hugepage_bits(size_t alloc_size) {\n // 检查 1GB 和 2MB 大页的可用数量\n // 返回足够支撑分配大小的最大页大小(bit 数)\n // 如 2MB = 21 bits, 1GB = 30 bits\n}\n\n\n七、AOTInductor C++ 部署完整流程NVE 支持将包含 NVEmbedding / NVEmbeddingBag 的 PyTorch 模型导出为无 Python 运行时的 C++ 可执行程序。流程分为两步:\nStep 1 — Python 导出\nfrom pynve.torch.nve_export import export_aot\n\nmodel = MyModel()\nexport_aot(model, (example_keys,), \"save_dir/\")\n\n生成的文件结构:\nsave_dir/\n├── model.pt2 # AOTInductor 编译后的计算图\n├── metadata.json # 每层配置(行数、维度、数据类型、cache 类型等)\n└── weights/\n ├── embedding_0.nve # 嵌入权重二进制文件\n └── embedding_1.nve\n\nStep 2 — C++ 加载与推理\n#include <torch/torch.h>\n#include <torch/csrc/inductor/aoti_package/model_package_loader.h>\n#include \"python/pynve/torch_bindings/nve_loader.hpp\"\n\n// LayerDirectory 读取 metadata.json,创建嵌入层,加载权重\nnve::LayerDirectory dir(\"save_dir/\");\n\n// 加载 AOT 编译后的模型\ntorch::inductor::AOTIModelPackageLoader loader(\"save_dir/model.pt2\");\n\n// 推理(无 Python 运行时)\nauto keys = torch::tensor({0L, 1L, 5L, 10L},\n torch::TensorOptions().dtype(torch::kInt64).device(torch::kCUDA));\nc10::InferenceMode mode;\nauto outputs = loader.run({keys});\n\n架构Python 导出阶段:\n PyTorch 模型 (NVEmbedding)\n ↓ torch.export\n ExportedProgram\n ↓ AOTInductor compile\n model.pt2 (so 文件 + nve_ops::embedding_lookup 算子)\n + metadata.json + .nve weights\n\nC++ 推理阶段:\n AOTIModelPackageLoader::run({keys})\n ↓ C10 Dispatcher\n nve_ops::embedding_lookup (STABLE_TORCH_LIBRARY)\n ↓ NVELayerRegistry (单例)\n LinearUVMEmbeddingLayer::lookup()\n ↓\n GPU Embedding Cache lookup\n\n关键组件\nlibnve-torch-ops.so:注册自定义算子 nve_ops::embedding_lookup 到 C10 dispatcher,使用 STABLE_TORCH_LIBRARY 宏实现 LibTorch Stable ABI 兼容\nNVELayerRegistry:全局单例,在 LayerDirectory 构造时将层 ID → 层指针的映射注册到 registry,AOT 模型通过层 ID 查找\nLayerDirectory:RAII 类,构造函数创建层、加载权重、注册到 registry;析构函数注销并销毁\n.nve 权重格式:NVE 自定义二进制格式,包含层元数据头 + 嵌入向量数据,支持快速内存映射加载\n\n链接要求g++ inference.cpp -o inference \\\n -L/path/to/nve/build/lib -lnve-torch-ops -lnve-common \\\n -L$(python -c \"import torch; print(torch.utils.cmake_prefix_path)\")/../lib \\\n -ltorch -ltorch_cpu -ltorch_cuda -lc10 \\\n -Wl,-rpath,/path/to/nve/build/lib\n\nlibnve-torch-ops.so 构建条件:必须启用 torch bindings(默认),且 PyTorch ≥ 2.10(Stable ABI 兼容性通过 cmake/check_torch_stable_abi.cpp 验证)。\n\n八、可插拔组件总结NVE 的三大可插拔基础设施:\n\n\n\n组件\n接口\n默认实现\n插件化方式\n\n\n\nThreadPool\nThreadPool\nSimpleThreadPool\ncreate_thread_pool(json) + configure_default_thread_pool(json)\n\n\nAllocator\nAllocator\nDefaultAllocator(cudaMalloc + 大页 mmap)\n构造函数参数传入\n\n\nInsertHeuristic\nInsertHeuristic\nDefaultInsertHeuristic(阈值 0.75)\nLayer Config 传入\n\n\n使用示例:\n// 自定义分配器\nclass MyAllocator : public Allocator { ... };\nauto allocator = std::make_shared<MyAllocator>();\nauto layer = std::make_shared<GPUEmbeddingLayer<int64_t>>(config, allocator);\n\n// 自定义线程池\nauto pool = create_thread_pool({ {\"num_workers\", 32} });\nconfigure_default_thread_pool({ {\"num_workers\", 32} });\n\n// 自定义插入策略\nauto heuristic = std::make_shared<StatisticalInsertHeuristic>(\n num_keys, std::vector<float>{2.6f});\nHierarchicalEmbeddingLayer<int64_t>::Config cfg;\ncfg.insert_heuristic = heuristic;","categories":["inference"],"tags":["NVIDIA","Embedding","AOTInductor","PyTorch","多GPU","CUDA","线程池","NVEmbeddingCache"]},{"title":"gpu-embedding-layer","url":"/posts/f20751de.html","content":"GPUEmbeddingLayer 实现分析概述GPUEmbeddingLayer 是 NV Embedding Cache SDK 中最直接、最精简的嵌入层实现。它将所有嵌入数据存储在 GPU 线性内存(Linear Memory)中,查找操作完全在 GPU 上完成,不涉及主机(Host)回退。其核心思路是:如果完整嵌入表可以装入显存,那么直接在 GPU 上进行查找是最快的选择,无需任何缓存或层级回退。\n该层位于 include/gpu_embedding_layer.hpp(声明)和 src/gpu_embedding_layer.cu(实现),模板参数为 KeyType,目前只支持 int32_t 和 int64_t 两种键类型。\n\n设计原理定位:无缓存的纯 GPU 嵌入层与 HierarchicalEmbeddingLayer(三级缓存)和 LinearUVMEmbeddingLayer(GPU 缓存 + UVM 线性内存)不同,GPUEmbeddingLayer 不管理任何缓存。它假设用户已经将完整嵌入表放在了 GPU 显存中,通过 config.embedding_table 指针传入。查找时直接通过 cuEmbed 库的 EmbeddingForward kernel 从线性表中读取数据,写入输出缓冲区。\n这对应了 README 中描述的第一种配置场景:\n\n所有嵌入都分配在线性 GPU 内存中:使用 GPUEmbeddingLayer(C++)/ 带 NoCache 缓存类型的 NVEmbedding(Python)。\n\n适用场景\n嵌入表规模较小,可以完整装入单张 GPU 显存\n追求最低的查找延迟(无需任何 host 回退或缓存未命中处理)\n推理场景的 baseline 配置,用于与其他缓存层做性能对比\n\n\n配置结构体:GPUEmbeddingLayerConfigstruct GPUEmbeddingLayerConfig {\n std::string layer_name; // 层名称(用于日志/调试)\n int device_id{0}; // 使用的 GPU 设备编号\n void* embedding_table; // GPU 线性内存中的嵌入表指针\n int64_t num_embeddings; // 嵌入表行数\n int64_t embedding_width_in_bytes; // 每行嵌入向量的字节宽度\n DataType_t value_dtype; // 存储数据类型(仅用于 accumulate)\n};\n\n关键说明:\n\nembedding_table 必须是 GPU 设备内存指针,由用户预先分配和填充\nembedding_width_in_bytes 目前只支持 fp16 和 fp32 类型,且必须能被 2 整除\n配置支持 JSON 序列化/反序列化(from_json / to_json)\n\n\n内部数据结构template <typename KeyType>\nclass GPUEmbeddingLayer : public EmbeddingLayerBase {\n private:\n GPUEmbeddingLayerConfig config_; // 层配置\n allocator_ptr_t allocator_; // 内存分配器\n\n std::mutex kernel_launch_mutex_; // 内核启动互斥锁\n std::shared_ptr<ContextRegistry> contexts_; // 执行上下文注册表\n cudaEvent_t modify_in_progress_; // 修改操作进行中的 CUDA 事件\n cudaStream_t private_modify_stream_; // 私有修改流\n};\n\n关键的同步基础设施:\n\nkernel_launch_mutex_:保护所有 kernel 启动和 CUDA 事件排队操作,防止多个线程同时向 GPU 流提交命令\ncontexts_:ContextRegistry 跟踪所有存活的执行上下文,用于在修改操作时同步所有查找流\nmodify_in_progress_:一个 cudaEventDisableTiming 事件,标记当前正在进行的修改操作。新发起的查找必须等待这个事件完成,保证读写一致性\nprivate_modify_stream_:一个专用于修改操作(update / accumulate)的 CUDA 流,与查找流的执行异步进行\n\n\n核心操作实现lookup() — 查找操作lookup() 是 GPUEmbeddingLayer 最核心的操作,完全在 GPU 上执行。完整调用链如下:\n用户调用 layer->lookup(ctx, num_keys, keys, output, ...)\n │\n ├── ① ScopedDevice: 切换当前 CUDA context 到目标 GPU\n │\n ├── ② BufferWrapper: 确保 keys 与 output 在 Device 内存中\n │\n ├── ③ kernel_launch_mutex_.lock()\n │ └── cudaStreamWaitEvent(lookup_stream, modify_in_progress_)\n │ 确保所有之前的修改操作已完成\n │\n ├── ④ 按 pooling 参数分流:\n │ ├── 无 pool_params → cuembed_find<KeyType>()\n │ └── 有 pool_params → cuembed_find_and_combine<KeyType>()\n │\n ├── ⑤ 如果 output 原本在 Host 上 → cudaMemcpyAsync 回传\n │\n └── ⑥ hitrates[0] = 1.0f\n\n① ScopedDevice — RAII 设备切换class ScopedDevice {\n ScopedDevice(int device_id) {\n cudaGetDevice(&curr_device_);\n if (device_id >= 0 && curr_device_ != device_id) {\n cudaSetDevice(device_id);\n }\n }\n ~ScopedDevice() {\n if (swap_device_) cudaSetDevice(curr_device_);\n }\n};\n\n这是一个 RAII 辅助类,确保 CUDA kernel 在正确的 GPU 设备上启动。构造函数中记录当前设备,如需切换则调用 cudaSetDevice,析构时恢复。\n② BufferWrapper — 透明内存适配器BufferWrapper(include/buffer_wrapper.hpp)是 NVE 的透明内存适配器,解决了一个关键问题:用户传入的 keys/output 指针可能在 Host 内存、Device 内存或 Unified Memory 上,而 CUDA kernel 需要 Device 指针。其核心状态机:\n构造时: 检测指针类型 (BufferType)\n │\n ┌─────┴──────┐\n │ buffers_ map│ key = cudaMemoryType\n │ │ value = 内存指针\n └─────────────┘\n │\n access_buffer(target_mem_type, copy_content, stream)\n │\n ┌─────┴─────────────────────────────────────┐\n │ 1. buffers_ 中是否已有 target 类型的副本? │\n │ ├── 有 → 直接返回 │\n │ └── 无 → ctx_->get_buffer() 分配 │\n │ │\n │ 2. copy_content = true? │\n │ ├── 是 → cudaMemcpyAsync 从最后访问 │\n │ │ 的缓冲区拷贝到目标缓冲区 │\n │ │ (如果是 Host 目标还需 stream sync) │\n │ └── 否 → 跳过拷贝 │\n │ │\n │ 3. last_access_ = target_mem_type │\n └─────────────────────────────────────────────┘\n\n关键设计:\n\nlast_access_ 记录最近一次访问的内存类型,作为默认的 copy source\nconst 缓冲区的优化:对于 const T* 包装器,copy_content 只在首次分配时有效,后续 access 不再重复拷贝\nHost 拷贝后同步:当目标内存类型是 cudaMemoryTypeHost 时,拷贝后调用 cudaStreamSynchronize(stream)\n内存所有权:分配的缓冲区的生命周期由 ExecutionContext 管理,BufferWrapper 不负责释放\n\n③ 内核启动锁与 modify 事件同步std::lock_guard lock(kernel_launch_mutex_); // 保护 CUDA API 调用的原子性\nNVE_CHECK_(cudaStreamWaitEvent(lookup_stream, modify_in_progress_));\n\nkernel_launch_mutex_ 是一个 std::mutex,确保在多个线程同时调用 lookup/update/accumulate 时,CUDA 事件和 kernel launch 操作不会并发执行。cudaStreamWaitEvent 则在 GPU 硬件层面建立依赖——lookup stream 上的后续操作会等待 modify_in_progress_ 事件完成才执行,不阻塞 CPU 线程。\n④ cuEmbed 的 EmbeddingForward kernel 分发从 call_cuembed_forward() 进入 cuembed::EmbeddingForward(),这是一个多层模板分发的函数:\n第一层:数据类型分发\nswitch (value_dtype) {\n case Float32: EmbeddingForward<float, float, KeyType, int64_t, false>(...);\n case Float16: EmbeddingForward<__half, __half, KeyType, int64_t, false>(...);\n}\n\n第二层:向量宽度分发 — DivideRowIntoVectors<ElemT>() 根据嵌入宽度决定每次加载的向量宽度:\nif (bytes_per_row % 16 == 0) bytes_per_load = 16; // float4 / half4\nelse if (bytes_per_row % 8 == 0) bytes_per_load = 8;\nelse if (bytes_per_row % 4 == 0) bytes_per_load = 4;\n\n第三层:合并模式 + 向量宽度组合 — 3 种模式 × 4 种宽度 = 12 个分支\n第四层:热点数 + 权重/偏移组合 — 通过宏展开为 8 个分支\n总共组合数:2(数据类型) × 12(模式×宽度) × 8(热点分支) = 192 种编译时特化的 kernel launch。\n启动参数:\nauto [element_per_load, threads_per_sample, samples_per_cta] =\n GetKernelLaunchParams<ElemT, IndexT>(embed_width, num_hots, is_weighted);\n\ndim3 launch_block(embed_width / element_per_load, samples_per_cta, 1);\ndim3 launch_grid((batch_size + samples_per_cta - 1) / samples_per_cta, 1, 1);\nsize_t smem_size = samples_per_cta * num_hots * sizeof(IndexT);\n\n\nblock.x = 每行需要的线程数(embed_width / element_per_load)\nblock.y = 每个 CTA 处理的样本数(samples_per_cta)\ngrid.x = 总 batch 数\n\n⑤ pooling 中的 CSR 路径当使用 CSR 格式时,num_hots=0 是 cuEmbed 约定:当 offsets != nullptr && num_hots == 0 时使用 CSR 语义,每个 bag 的边界由 offsets 数组指示。\n⑥ hitrates由于 GPU 表是线性表,所有合法键都能直接访问(无缓存穿透概念),所以命中率固定为 1.0f。\nif (hitrates) hitrates[0] = 1.0f;\n\nupdate() — 更新操作update() 用于覆盖现有嵌入向量的值。完整同步序列:\nupdate(ctx, num_keys, keys, ...)\n │\n ├── BufferWrapper 确保 keys/values 在 Device\n │\n ├── kernel_launch_mutex_.lock()\n │\n ├── cudaStreamWaitEvent(private_modify_stream_, modify_in_progress_)\n │ 等待之前所有的 modify 完成\n │\n ├── StreamCoordinator sc(modify_stream, private_modify_stream_)\n │ 构造时: modify_stream → private_modify_stream_ (事件依赖)\n │\n ├── syncEvent = contexts_->create_sync_event()\n │ syncEvent->event_record()\n │ syncEvent->event_wait_stream(private_modify_stream_)\n │ 等待所有进行中的查找完成\n │\n ├── UpdateTable<KeyType>(..., private_modify_stream_)\n │ 在私有修改流上执行更新 kernel\n │\n ├── cudaEventRecord(modify_in_progress_, private_modify_stream_)\n │ 标记修改完成\n │\n └── cudaStreamWaitEvent(modify_stream, modify_in_progress_)\n ~StreamCoordinator: private_modify_stream_ → modify_stream_\n\n这里存在三重同步:\n\n\n\n序号\n同步原语\n作用\n\n\n\n①\ncudaStreamWaitEvent(priv_mod, modify_in_progress)\n等待上一个修改完成(写后写串行化)\n\n\n②\nsyncEvent->event_wait_stream(priv_mod) + contexts_ 的所有查找流\n等待所有查找流排空(读后写一致性)\n\n\n③\ncudaEventRecord(modify_in_progress, priv_mod) + 后续 wait\n通知后续查找/修改:当前修改已完成\n\n\nUpdateTableKernel 是一个 CUDA kernel,位于 cuda_ops/update_accumulate.cuh。它使用 subwarp 粒度的并行策略:每个 subwarp(8 或 16 个线程)处理一个索引,线程间按元素并行,将源值拷贝到嵌入表的对应行。\naccumulate() — 梯度累积操作accumulate() 用于反向传播场景,将梯度值累加到嵌入向量上(而不是覆盖)。与 update() 相似的流程,但调用的是 UpdateAccumulateTableKernel,核心差异在于使用 atomicAdd 而非直接赋值:\nfor (int el = threadIdx.x; el < embed_width; el += SubwarpWidth) {\n atomicAdd(embed_dst + el, embed_src[el]); // 原子累加\n}\n\n支持两种梯度精度:\n\n\n\n梯度类型\nDataType\n原子操作粒度\n\n\n\nFloat32\nfloat\natomicAdd(float*)\n\n\nFloat16\n__half\natomicAdd(half2*)\n\n\ninsert() / erase() / clear()这三个方法在 GPUEmbeddingLayer 中没有实际效果,只打印警告日志:\n\ninsert():输出 “insert method has no effect for GPU layer, use update to change table content”\nerase():输出 “erase method has no effect for GPU layer”\nclear():输出 “clear method has no effect for GPU layer”\n\n原因在于该层只是对用户已经分配好的 GPU 线性内存的一个视图(View),不拥有嵌入表的所有权,也不管理其生命周期。用户通过 config.embedding_table 传入指针,NVE 层不对表内容做增删操作。\n\n流同步与并发模型GPUEmbeddingLayer 的并发模型围绕三个关键组件构建。\nkernel_launch_mutex_一个 std::mutex,保护所有 CUDA API 调用(cudaStreamWaitEvent、cudaEventRecord、kernel launch 等)。在 update() 和 accumulate() 中与 StreamCoordinator 配合使用,确保事件记录和等待的原子性。\nmodify_in_progress_ 事件一个 cudaEventDisableTiming 事件,标记最近一次修改操作的完成时间点:\n\nupdate() / accumulate() 提交时:在修改 stream 上记录事件\nlookup() 发起时:在 lookup stream 上等待该事件 → 保证读后写(read-after-write)一致性\n下一个 update() / accumulate() 发起时:也在修改 stream 上等待该事件 → 保证写后写(write-after-write)顺序\n\nContextRegistry 同步在 update() / accumulate() 中,修改 kernel 启动之前会通过 contexts_->create_sync_event() 创建一个同步事件,该事件会在所有已知的 lookup stream 上等待,确保没有正在执行的查找操作读取即将被修改的数据。\nStreamCoordinatorStreamCoordinator 是一个 RAII 辅助类,在构造和析构时在两个 CUDA 流之间建立事件依赖:\nStreamCoordinator sc(modify_stream, private_modify_stream_);\n// 构造时:modify_stream → private_modify_stream_\n// 析构时:private_modify_stream_ → modify_stream_\n\ncreate_stream_dependency 使用临时 cudaEvent(cudaEventDisableTiming 轻量事件,创建开销 ~0.21μs):\nstatic void create_stream_dependency(const cudaStream_t& src,\n const cudaStream_t& dst) {\n cudaEvent_t e;\n cudaEventCreateWithFlags(&e, cudaEventDisableTiming);\n cudaEventRecord(e, src);\n cudaStreamWaitEvent(dst, e);\n cudaEventDestroy(e);\n}\n\n线程安全模型总结 Thread 1 Thread 2\n │ │\n lookup(ctx1) lookup(ctx2)\n │ │\n lock(mutex) lock(mutex)\n │ │\nwaitEvent(modify_in_prog) waitEvent(modify_in_prog)\n │ │\n launch kernel launch kernel\n │ │\n unlock(mutex) unlock(mutex)\n │ │\n lookup 完成 lookup 完成 (并发执行)\n │ │\n ├───────────────────────┤\n │\n update(ctx1)\n │\n lock(mutex)\n │\n waitEvent(modify_in_prog)\n │ ┌─ contexts_ sync event\n │ │ (等待所有 lookup stream 排空)\n │ └─ StreamCoordinator\n │\n launch UpdateTable\n │\n recordEvent(modify_in_prog)\n │\n unlock(mutex)\n\n\n多个 lookup 可以并发:在不同的 GPUEmbeddingTableExecutionContext 上\nupdate/accumulate 与 lookup 间:通过 modify_in_progress_ event 实现 GPU 级异步同步\nupdate/accumulate 间的串行化:通过 waitEvent + recordEvent 保证写后写顺序\nkernel_launch_mutex_:保护 CUDA API 调用的原子性,防止多线程同时创建/销毁 event\n\n\nUpdate / Accumulate Kernel 的 Subwarp 并行UpdateTableKerneltemplate<uint32_t SubwarpWidth, typename KeyType, typename DataType>\n__global__ void UpdateTableKernel(...) {\n const int id = blockIdx.x * blockDim.y + threadIdx.y; // 全局索引 ID\n if (id >= num_indices) return;\n\n KeyType key = indices[id];\n const DataType* embed_src = src + id * embed_src_stride_in_bytes;\n DataType* embed_dst = embedding_table + key * embed_dst_stride_in_bytes;\n\n // subwarp 内按元素并行\n for (int el = threadIdx.x; el < num_elements; el += SubwarpWidth) {\n embed_dst[el] = embed_src[el];\n }\n}\n\n启动参数:grid_size = (num_indices + indices_per_warp - 1) / indices_per_warp,block_size = (SubwarpWidth, indices_per_warp),其中 indices_per_warp = 32 / SubwarpWidth。\n以 64 维 fp32 嵌入为例:SubwarpWidth = 32,indices_per_warp = 1,block_size = (32, 1)。每个 block 使用 32 个线程,每个线程处理 2 个元素,一个 block 处理 1 个索引。\nSubwarpWidth 的编译时选择uint32_t subgroupWidth = std::min(nextPow2(embed_width / sizeof(DataType)), 32u);\nswitch (subgroupWidth) {\n case 32: CallUpdateKernelVecTypeSubwarp<32, ...>(...); break;\n case 16: ...;\n case 8: ...;\n case 4: ...;\n case 2: ...;\n case 1: ...;\n}\n\nnextPow2 返回大于等于输入的最小 2 的幂,截断到 32(warp 大小),确保 subwarp 宽度与嵌入维度对齐。\n向量化加载的 dispatchif (embed_width_in_bytes % 16 == 0)\n CallUpdatKernelVecType<KeyType, Vec4>(...); // float4 / half4\nelse if (embed_width_in_bytes % 8 == 0)\n CallUpdatKernelVecType<KeyType, Vec2>(...); // float2 / half2\nelse if (embed_width_in_bytes % 4 == 0)\n CallUpdatKernelVecType<KeyType, Vec1>(...); // float\n\n通过 VecWidthHelper 模板特化,将标量类型映射到向量类型。向量化加载能让编译器生成 LDG.E.128 / STG.E.128 指令,一次加载 16 字节,最大化内存带宽利用率。\n\nGPUEmbeddingTableExecutionContextGPUEmbeddingTableExecutionContext 是 ExecutionContext 的定制子类,每个执行上下文包含一对 CUDA 流(lookup_stream 和 modify_stream),多个上下文可以并发执行查找。\nclass GPUEmbeddingTableExecutionContext : public ExecutionContext {\n GPUEmbeddingTableExecutionContext(...)\n : ... {\n context_registry_->add_context(this); // 注册到 registry\n }\n\n ~GPUEmbeddingTableExecutionContext() {\n cudaStreamSynchronize(lookup_stream_);\n cudaStreamSynchronize(modify_stream_);\n context_registry_->remove_context(this); // 注销\n }\n};\n\n关键点:\n\n构造时自动注册:将自身加入 ContextRegistry,使修改操作能同步到本上下文\n析构时自动同步:确保所有流上的待处理工作完成后再销毁\n缓冲区生命周期:从 ExecutionContext 继承的 buffer_storage_ 管理所有临时缓冲区\n\n\ncuEmbed EmbeddingForward Kernel 的内部结构cuEmbed 的 EmbeddingLookUpKernel 的核心逻辑是 共享内存协作式查找:\n对于每个 CTA 处理的 samples_per_cta 个样本:\n │\n ├── 1. 从 global memory 加载索引到共享内存\n │\n ├── 2. 屏障同步 (__syncthreads)\n │\n ├── 3. 对于嵌入向量中的每个元素 (由 threadIdx.x 负责):\n │ a. 从 params + indices[sample * num_hots + hot] * embed_width 读取\n │ b. 如果有权重: 读取权重并 MulAccumulate\n │ c. 否则: 直接 Accumulate\n │ d. 遍历 num_hots 个热点\n │\n ├── 4. 如果 mode == kMean: 除以 num_hots\n │\n └── 5. 写入输出 ret + sample * output_width\n\nAddresser 模板负责地址计算,Combiner 模板负责累加/拼接/均值计算,IndexLoader 模板负责从共享内存加载索引。三者的编译时特化组合构成了全部 192 种 kernel 变体。对于 kConcat 模式,kernel 跳过累加阶段,直接将每个热点的嵌入向量连续写入输出缓冲区。\n\n与其余嵌入层的对比\n\n\n特性\nGPUEmbeddingLayer\nLinearUVMEmbeddingLayer\nHierarchicalEmbeddingLayer\n\n\n\n存储位置\n全 GPU 显存\nGPU 缓存 + UVM 线性内存\nGPU 缓存 + CPU 内存 + 远程\n\n\n缓存机制\n无\nGPU 集合关联缓存\n三级缓存层级\n\n\n查找路径\nGPU kernel 直读\nGPU cache → UVM fallback\nGPU → CPU → Remote\n\n\nhost fallback\n无\n有(UVM 缺页)\n有(CPU 表 + 远程表)\n\n\ninsert/erase\n无效果(warn)\n有\n有\n\n\n适用场景\n小表,全显存\n中表,超显存但可 UVM\n大表,远超显存\n\n\n查找延迟\n最低\n中等(cache hit 时低)\n较高(miss 时回退多)\n\n\nlookup kernel\ncuEmbed EmbeddingForward\nEmbedCacheSA + cuEmbed\nEmbedCacheSA + CPU gather\n\n\n映射关系\n1:1 直接寻址\ncache tag + UVM page\ncache tag + hash table + remote\n\n\n命中率报告\n固定 1.0\n需 stream sync 统计\n需 stream sync 统计\n\n\n同步复杂度\n最低\n中等\n最高\n\n\n\n总结GPUEmbeddingLayer 是 NVE 中最简单、最直接的嵌入层实现。它假定嵌入表已完整驻留在 GPU 显存中,通过 cuEmbed 的优化 kernel 实现零回退的极速查找。其代码量虽小(约 250 行 CUDA C++),但展示了完整的 CUDA 流同步模式(kernel_launch_mutex_ + modify_in_progress_ event + ContextRegistry + StreamCoordinator),为更复杂的 LinearUVMEmbeddingLayer 和 HierarchicalEmbeddingLayer 奠定了基础。\n从底层实现来看,GPUEmbeddingLayer 本质上是一个直接索引 + 向量加载的 CUDA kernel wrapper。其查找路径涵盖了 ScopedDevice 设备管理、BufferWrapper 透明内存适配、cuEmbed 的 192 种 kernel 特化分发、以及多级 CUDA 流同步等关键技术点。对于嵌入表可以完整装入显存的场景(如 Criteo 数据集的小型模型或精排阶段),GPUEmbeddingLayer 是性能最优的选择。\n","categories":["inference"],"tags":["NVIDIA","Embedding","GPU","CUDA","NVEmbeddingCache"]},{"title":"hierarchical-embedding-layer","url":"/posts/ff05b45.html","content":"HierarchicalEmbeddingLayer 深度分析概述HierarchicalEmbeddingLayer 是 NV Embedding Cache SDK 中 最复杂、功能最完整 的嵌入层实现。它组合了多张表(tables)构成查找层级,按照”先快后慢”的顺序逐级查找:首先尝试 GPU 集合关联缓存,未命中则转发到 CPU 端哈希表,仍未命中则转发到远程存储(Redis / RocksDB)。每张表独立管理各自的存储后端,层负责协调表间的数据流动和缺失键转发。\n该层对应 README 中的第三种配置场景:\n\n一些嵌入缓存在 GPU 内存中,一些缓存在 Host 内存中,所有嵌入保留在远程参数服务器:使用带 Hierarchical 缓存类型的 NVEmbedding(Python)/ HierarchicalEmbeddingLayer(C++)。\n\n文件位置:include/hierarchical_embedding_layer.hpp(声明)和 src/hierarchical_embedding_layer.cu(实现)。\n\n核心概念与架构多表级联(Cascade)HierarchicalEmbeddingLayer 接收一个 std::vector<table_ptr_t> 作为构造参数,这个有序列表定义了查找的优先级顺序:\ntables_[0] → GPU 集合关联缓存(GpuTable,最快)\n ↓ miss\ntables_[1] → CPU 哈希表(HostTable,如 nvhm_map / abseil_flat_map / stl_umap)\n ↓ miss\ntables_[2] → 远程存储(RedisClusterTable / RocksDBTable,最慢但容量最大)\n\n设计约束:\n\n至少需要一张表(NVE_CHECK_(tables_.size() > 0))\nGPU 表必须连续排列在前(不允许 CPU 表之后再有 GPU 表,因为无性能意义)\n所有表的行大小必须一致(get_max_row_size() 相同)\n可选提供默认嵌入向量(default_embedding),用于在所有表都 miss 时的回退值。此时最后一张表必须是 host table\n\nConfig 结构体struct Config {\n std::string layer_name;\n std::shared_ptr<InsertHeuristic> insert_heuristic = nullptr;\n int64_t min_insert_freq_gpu = 0;\n int64_t min_insert_freq_host = 0;\n int64_t min_insert_size_gpu = 1 << 16; // 65536\n int64_t min_insert_size_host = 0;\n std::vector<uint8_t> default_embedding = {};\n};\n\n与 LinearUVMEmbeddingLayer 相比,增加了:\n\nmin_insert_freq_host:Host 表的最小插入间隔频率\nmin_insert_size_host:Host 表的最小插入批量(默认 0,即每次有 miss 就立即插入)\ndefault_embedding:所有表都 miss 时的默认值。默认空表示缺失键行为未定义\n\n\nlookup() 查找流程的完整实现HierarchicalEmbeddingLayer::lookup() 是整个 NVE 中最复杂的操作,其核心逻辑围绕每张表独立工作、逐级传递 hitmask 展开。\n数据流全景用户调用 layer->lookup(ctx, num_keys, keys, output, ...)\n │\n ├── ① ScopedDevice: 切换到 GPU\n │\n ├── ② 分配 hitmask 缓冲区,初始化为 0\n │ hitmask[i] = 0 → 键 i 尚未被任何表命中\n │\n ├── ③ 遍历每张表,执行查找:\n │ for i = 0; i < num_tables; i++:\n │ │\n │ ├── 重置该表的 lookup counter\n │ │\n │ ├── table->find_bw(ctx, num_keys, keys_bw, hitmask_bw,\n │ │ output_stride, output_bw, value_sizes)\n │ │ └── 表在查找时跳过 hitmask 中已标记为 1 的键\n │ │ └── 命中后在 hitmask 中将对应位设为 1\n │ │ └── 将命中键的数据写入 output 的对应位置\n │ │\n │ └── table->get_lookup_counter() 获取该表的命中数\n │\n ├── ④ 同步与命中率计算:\n │ │ 至少有一张 GPU 表时需要 cudaStreamSynchronize\n │ │ 计算每张表的 hitrate = hits / left_keys\n │ │ left_keys = num_keys - 已累加的所有 hits\n │\n ├── ⑤ 默认嵌入填充:\n │ if (left_keys > 0 && default_embedding 非空)\n │ 在 CPU 线程池中并行填充未命中位置的默认值\n │\n ├── ⑥ GPU scatter(关键步骤):\n │ if (首表是 GPU && 末表是 Host)\n │ 调用 EmbeddingForwardScatter kernel\n │ 将 Host 端填充的数据 scatter 回 Device 输出缓冲区\n │\n ├── ⑦ 如果 output 不在最终位置 → cudaMemcpyAsync\n │\n ├── ⑧ 自动插入:\n │ for 每张表:\n │ auto_insert_handlers_[i]->auto_insert(...)\n │ 收集该表的 miss 键 → 达到阈值后批量插入\n │\n └── ⑨ pooling → NVE_THROW_NOT_IMPLEMENTED_()\n\nhitmask 驱动的级联查找hitmask 是整个级联查找的核心数据结构。它是一个大位图,每个 bit 对应一个 key:\n\nbit = 0:该键尚未被任何表命中,需要传递给当前表处理\nbit = 1:该键已被前面的表命中,当前表应跳过\n\n初始化:\n// 分配 hitmask 缓冲区\nconst auto hitmask_elements = (num_keys + 63) / 64;\nconst auto hitmask_buffer_size = hitmask_elements * sizeof(max_bitmask_repr_t);\n\n// 零初始化(所有 bit = 0,即全未命中)\nif (hitmask_first_access == cudaMemoryTypeDevice) {\n cudaMemsetAsync(hitmask_buf, 0, hitmask_buffer_size, lookup_stream);\n} else {\n std::memset(hitmask_buf, 0, hitmask_buffer_size);\n}\n\n每张表的 find_bw() 实现会做两件事:\n\n只处理 hitmask 中 bit=0 的键(跳过已命中键)\n对命中的键,将 hitmask 对应 bit 设为 1,并将数据写入 output\n\nGpuTable 的 find_bw 在 UVM 回退查找(run_find_uvm)中同时处理 cache hit 和 UVM miss:cache tag 匹配则从 cache 数据区读取,否则从 UVM table 读取;两者都命中时标记 hitmask bit=1。\nHostTable 的 find 实现(如 STLContainerTable::find)逐分区并行查找,仅在 hitmask bit=0 的条目上耗费查找资源。\nhitmask 的数据传输路径级联查找中,hitmask 可能需要在 GPU 和 CPU 之间传输:\n场景 1: 首表 GPU + 次表 GPU → hitmask 全程在 Device\n场景 2: 首表 GPU + 次表 Host → hitmask 需从 Device 回传到 Host\n场景 3: 首表 Host + 次表 Host → hitmask 全程在 Host\n\nBufferWrapper 的 access_buffer() 透明处理了这种传输:\nauto* hitmask_host = hitmask_bw->access_buffer(\n cudaMemoryTypeHost, true /*copy_content*/, lookup_stream);\n// hitmask 当前在 Device → 自动 cudaMemcpyAsync 到 Host\n\n\nGpuTable 在层级中的角色当 GpuTable 作为层级的首表时,其 find_bw 方法会:\n\n执行 EmbedCacheSA 查找:GPU 集合关联缓存标签匹配\nUVN 回退:如果 uvm_table 非空,cache miss 时直接从 UVM 内存读取(run_find_uvm)\n更新 hitmask:所有在 UVM 中找到的键(cache hit + UVM hit)标记为已命中\n计数 miss:仅记录 cache miss 的数量(传递给 auto_insert_handler 作为 GPU 插入决策依据)\n\n当 GpuTable 作为非首表(即前面已经有其他表)时,其 find_bw 的 hitmask 会包含来自前面所有表的命中信息——它只需要处理前面表未能命中的键。\n\nCPU 端 HostTable 的查找与 gather对于 CPU 端的 host table,查找流程在 cpu_ops/cpu_gather.h 中的 cpu_kernel_gather 实现:\ntemplate<typename IndexT>\nint cpu_kernel_gather(thread_pool_ptr_t thread_pool,\n uint64_t n,\n const IndexT* keys,\n max_bitmask_repr_t* hit_mask,\n size_t value_stride,\n void* values,\n int8_t* cpu_table_ptr,\n size_t row_size_in_bytes,\n uint64_t num_threads)\n{\n const auto gather_task = [=] (const size_t idx) {\n for (uint64_t i = 0; i < keys_per_task; i++) {\n if ((hit_mask[...] & bit_mask) == 0) {\n IndexT key = keys[...];\n memcpy(dst_ptr, src_ptr, row_size_in_bytes);\n }\n }\n };\n thread_pool->execute_n(0, num_threads, gather_task);\n\n // 全部完成后统一设置所有 hitmask bit=1\n memset(hit_mask, 0xff, ...);\n return 0;\n}\n\n关键点:\n\n线程池并行:通过 thread_pool->execute_n 将工作分派到多个 CPU 核心\nmemcpy 直接读取:从线性 CPU 内存中 memcpy 嵌入向量到 output\n批量设置 hitmask:全部完成后统一设置所有 bit 为 1,表示这些键已被 host table 完全处理\n对齐优化:每个任务的 keys 数对齐到 64(hitmask 的 bit 边界)\n\n对于基于哈希表的 host table(如 STLContainerTable 或 AbseilFlatMapTable),查找流程是先通过哈希表查询键是否存在,如果存在则从对应的内存槽中拷贝数据。\n\nEmbeddingForwardScatter — GPU scatter kernel当层级结构为首表 GPU + 末表 Host 时,最终的查找数据可能散落在 Host 和 Device 两处:GPU cache 命中的数据在 Device 上,CPU host table 填充的数据在 Host 上。EmbeddingForwardScatter kernel 负责将 Host 数据 scatter 回 Device 输出缓冲区。\nkernel 定义在 cuda_ops/scatter.cuh:\ntemplate<uint32_t SubwarpWidth, typename DataType>\n__global__ void EmbedScatter(\n const int8_t* __restrict__ src, // Host 端源数据\n int8_t* __restrict__ dst, // Device 端目标输出\n const uint32_t embed_width_in_bytes,\n const uint64_t* __restrict__ hit_mask, // GPU 端 hitmask\n const int32_t num_indices)\n{\n const int embed = blockIdx.x * blockDim.y + threadIdx.y;\n if (embed >= num_indices) return;\n\n // 检查 hitmask: 如果该键已被 GPU 命中 → 跳过\n const int mask_entry = embed / 64;\n const int mask_bit = embed % 64;\n if (hit_mask[mask_entry] & (1ULL << mask_bit)) {\n return; // GPU 已命中,无需 scatter\n }\n\n // 只有 GPU 未命中的键 → 从 Host 源拷贝到 Device 目标\n memcpy_warp<SubwarpWidth, DataType>(dst, src, embed_width_in_bytes);\n}\n\n这个 kernel 的精妙之处在于:\n\n只传输缺失数据:GPU 已命中的键不参与拷贝,节省带宽\nSubwarp 并行:使用 memcpy_warp 以 subwarp 粒度高效拷贝\n向量化加载:根据对齐情况选择 float4/float2/float 等加载粒度\n\n\n默认嵌入向量填充当所有表都 miss 且配置了 default_embedding 时,使用 CPU 线程池并行填充:\nif ((left_keys > 0) && (config_.default_embedding.size() > 0)) {\n auto* hit_mask_buf = hitmask_bw->access_buffer(cudaMemoryTypeUnregistered, true, stream);\n auto* output_buf = output_bw->access_buffer(cudaMemoryTypeUnregistered, true, stream);\n\n const auto fill_default_task = [=](const int64_t idx) {\n for (int64_t k = start_key; k < end_key; k++) {\n if (hit_mask_buf[k / 64] 的第 k%64 位 == 0) {\n memcpy(output_bytes + k * output_stride, default_emb, row_size);\n }\n }\n };\n thread_pool->execute_n(0, num_tasks, fill_default_task);\n}\n\n重要约束:使用 default_embedding 时最后一张表必须是 host 表,因为默认值填充在 CPU 上执行。如果最后是 GPU 表,则无法保证所有键能被覆盖。\n\n命中率计算与报告left_keys = num_keys;\nfor (size_t i = 0; i < num_tables; i++) {\n if (tables_[i]->lookup_counter_hits() == false) {\n table_hits[i] = left_keys - table_hits[i]; // miss → hit\n }\n table_hitrates[i] = (float)table_hits[i] / left_keys;\n left_keys -= table_hits[i];\n}\n\n关键观察:\n\nGpuTable 计数的是 miss(lookup_counter_hits() == false)\nHostTable 计数的是 hit(lookup_counter_hits() == true)\nhitrates[i] 表示第 i 张表在当前剩余未命中键中的占比\n最终 hitrates 数组被归一化为相对于 num_keys 的比例\n\n\nAutoInsertHandler 的多表协同HierarchicalEmbeddingLayer 为每张表各维护一个 AutoInsertHandler:\nstd::vector<std::shared_ptr<AutoInsertHandler>> auto_insert_handlers_;\n\n在构造函数中创建,每层的参数独立:\nfor (size_t i = 0; i < tables_.size(); i++) {\n bool gpu_table = (table->get_device_id() >= 0);\n auto_insert_handlers_.push_back(std::make_shared<AutoInsertHandler>(\n heuristic,\n table, i, allocator_,\n gpu_table ? config_.min_insert_freq_gpu : config_.min_insert_freq_host,\n gpu_table ? config_.min_insert_size_gpu : config_.min_insert_size_host,\n sizeof(KeyType), gpu_device_, &invalid_key\n ));\n}\n\n在 lookup() 末尾触发自动插入:\nfor (size_t i = 0; i < num_tables; i++) {\n auto_insert_handlers_[i]->auto_insert(\n layer_ctx, keys_bw, output_bw, table_hitrates[i],\n num_keys, output_stride,\n (left_keys > 0) ? hitmask_bw : nullptr\n );\n}\n\n默认 InsertHeuristic 的阈值设置for (size_t i = 0; i < tables_.size(); i++) {\n if (i == last_host_idx) {\n thresholds.push_back(0.0f); // 最后一张 host 表永不触发插入\n } else {\n thresholds.push_back(DEFAULT_THRESHOLD); // 0.75\n }\n}\n\n这个设计很精妙:最后一张 host 表的阈值为 0.0,意味着系统不会试图向最后的持久化存储层(如 RocksDB 或 Redis)自动插入数据,因为该层已经拥有全量数据。\n\ninsert() 实现的区别与 LinearUVMEmbeddingLayer(固定 table_id=0)不同,HierarchicalEmbeddingLayer::insert() 接受 table_id 参数指定目标表:\nvoid insert(context_ptr_t& ctx, const int64_t num_keys, const void* keys,\n ..., const int64_t table_id) override {\n if (table_id < 0 || table_id >= tables_.size()) {\n NVE_LOG_INFO_(\"Insert called with invalid table_id - ignored call\");\n return;\n }\n auto& table = tables_.at(table_id);\n table->insert_bw(table_ctx, num_keys, keys_bw, value_stride, value_size, values_bw);\n}\n\n这允许用户精确控制数据插入到哪一层:插入到 GPU cache(table_id=0)、插入到 CPU host table(table_id=1)或插入到远程存储(table_id=2)。\n\nupdate() / accumulate() 的多表广播update() 和 accumulate() 会广播到所有表:\nvoid update(...) {\n for (size_t i = 0; i < tables_.size(); i++) {\n if (!auto_insert_handlers_.empty()) {\n auto_insert_handlers_[i]->lock_modify();\n }\n table->update_bw(table_ctx, num_keys, keys_bw, value_stride, value_size, values_bw);\n if (!auto_insert_handlers_.empty()) {\n auto_insert_handlers_[i]->unlock_modify();\n }\n }\n}\n\n设计取舍:\n\n每张表收到完整的 keys 和 values,而不是只发送 miss 的键\n每张表只更新自身已有条目\n自动插入 handler 在 update 期间被锁定,防止插入与更新的并发冲突\n\n\n执行上下文创建template <typename KeyType>\ncontext_ptr_t HierarchicalEmbeddingLayer<KeyType>::create_execution_context(...) {\n std::vector<context_ptr_t> table_contexts;\n for (auto& t : tables_) {\n auto ctx = t->create_execution_context(lookup_stream, modify_stream,\n thread_pool, actual_allocator);\n table_contexts.push_back(std::move(ctx));\n }\n return std::make_shared<LayerExecutionContext>(\n lookup_stream, modify_stream, thread_pool, actual_allocator, table_contexts);\n}\n\n每个子表各自创建自己的 ExecutionContext,统一包装在 LayerExecutionContext 中。在 lookup 时通过 layer_ctx->table_contexts_.at(i) 访问。\n\n与 LinearUVMEmbeddingLayer 的架构对比\n\n\n维度\nLinearUVMEmbeddingLayer\nHierarchicalEmbeddingLayer\n\n\n\n层级数\n2 层(GPU cache + UVM table)\nN 层(可扩展)\n\n\n第二级存储\nUVM 线性内存(硬件管理)\nCPU 哈希表 + 远程表(软件管理)\n\n\n二级访问\nGPU 直接读取(UVM page fault)\nCPU 线程池 + memcpy 或网络\n\n\nhitmask 用法\n无(GpuTable 内部使用)\n核心数据结构,层间传递\n\n\nscatter\n无(全程 GPU 端)\nEmbeddingForwardScatter kernel\n\n\n插入策略\n只针对 GPU cache\n每层独立 AutoInsertHandler\n\n\ndefault_embedding\n不支持\n支持\n\n\npooling\n支持(find_and_combine)\n暂未实现(throw)\n\n\nupdate/accumulate\n单表更新\n多表广播\n\n\n\n典型配置示例C++ API// 1. GPU cache\nGPUTableConfig gpu_cache_cfg;\ngpu_cache_cfg.device_id = 0;\ngpu_cache_cfg.cache_size = 1ULL << 31; // 2GB GPU cache\ngpu_cache_cfg.row_size_in_bytes = 128;\ngpu_cache_cfg.uvm_table = uvm_table;\ngpu_cache_cfg.count_misses = true;\nauto gpu_table = std::make_shared<GpuTable<int64_t>>(gpu_cache_cfg);\n\n// 2. CPU host table (RocksDB-backed)\nauto json_config = R\"({\n \"table_factories\": {\n \"my_rocksdb\": {\n \"implementation\": \"rocksdb\",\n \"path\": \"/data/embeddings/\",\n \"read_only\": true\n }\n },\n \"tables\": {\n \"0\": \"my_rocksdb\"\n }\n})\"_json;\nauto host_db = build_host_database(json_config);\nauto host_table = host_db.at(0);\n\n// 3. Hierarchical embedding layer\nstd::vector<table_ptr_t> tables = {gpu_table, host_table};\n\nHierarchicalEmbeddingLayer<int64_t>::Config cfg;\ncfg.layer_name = \"my_hier_layer\";\ncfg.min_insert_size_gpu = 65536;\n\nHierarchicalEmbeddingLayer<int64_t> layer(cfg, tables);\n\nPython APIimport pynve\n\nembedding = pynve.NVEmbedding(\n num_embeddings=1_000_000_000,\n embedding_dim=128,\n cache_type=\"Hierarchical\",\n cache_size=2_000_000_000, # 2GB GPU cache\n host_cache_size=50_000_000_000, # 50GB host cache\n)\n\n\n性能特征查找延迟分布\n\n\n场景\n延迟\n说明\n\n\n\nGPU cache hit\n最低\n纯 GPU kernel,几微秒\n\n\nGPU cache miss → UVM hit\n中\nUVM page fault,几十微秒\n\n\nGPU miss → CPU host table hit\n较高\nhost 回传 + 线程池查找,百微秒级\n\n\n所有表 miss + default\n最高\n线程池填充,取决于并行度\n\n\n关键调优参数\n\n\n参数\n作用\n调优建议\n\n\n\ntables_ 顺序\n先快后慢\nGPU cache → host table → remote\n\n\nmin_insert_size_gpu\nGPU 缓存插入批量\n增大则插入更少、batch 更大\n\n\nmin_insert_size_host\nHost 表插入批量\n默认 0(立即插入),大表可适当增大\n\n\ndefault_embedding\n所有表 miss 时的回退值\n推荐系统可设为全 0 向量\n\n\nInsertHeuristic 阈值\n各层触发频率\n默认 0.75,可统计调整\n\n\n\n总结HierarchicalEmbeddingLayer 是 NVE 三级存储层级(GPU → CPU → Remote)的最高层封装。其核心机制是 hitmask 驱动的级联查找——每张表只处理前面表未命中的键,避免了重复查找。\n与 LinearUVMEmbeddingLayer 的 UVM 硬件透明回退不同,HierarchicalEmbeddingLayer 的层间数据流需要显式的 hitmask 传递和 scatter scatter 操作,代码复杂度更高,但也带来了更大的灵活性:用户可以在层级中插入任意类型的表(包括远程 Redis/RocksDB),甚至为每层配置独立的自动插入策略。对于远超显存和系统内存的超大规模嵌入表场景(数十亿到数万亿的 Embedding ID),HierarchicalEmbeddingLayer 是唯一的选择。\n","categories":["inference"],"tags":["NVIDIA","Embedding","GPU","推荐系统","CUDA","NVEmbeddingCache"]},{"title":"linear-embedding-layer","url":"/posts/5d3fe4ad.html","content":"LinearUVMEmbeddingLayer 深度分析概述LinearUVMEmbeddingLayer 是 NV Embedding Cache SDK 中第二层复杂度的嵌入层实现。它通过组合 GPU 集合关联缓存(EmbedCacheSA)与 UVM(Unified Virtual Memory)线性后备表(UVM table),实现了”GPU 缓存命中则极速返回,缓存未命中则自动回退到 UVM 内存”的混合查找路径。查找过程完全在 GPU 上完成,无需回退到 CPU(与 HierarchicalEmbeddingLayer 不同)。\n该层对应 README 中的第二种配置场景:\n\n一些嵌入缓存在 GPU 内存中,所有嵌入在系统或 GPU 的线性内存中:使用带 LinearUVM 缓存类型的 NVEmbedding(Python)/ LinearUVMEmbeddingLayer(C++)。\n\n文件位置:include/linear_embedding_layer.hpp(声明)和 src/linear_embedding_layer.cu(实现)。\n\n与 GPUEmbeddingLayer 的核心差异\n\n\n维度\nGPUEmbeddingLayer\nLinearUVMEmbeddingLayer\n\n\n\n数据存储\n用户传入的 GPU 线性指针\nGPU 集合关联缓存 + UVM 线性表\n\n\n查找路径\ncuEmbed 直读\nEmbedCacheSA::lookup + UVM fallback\n\n\ninsert/erase\nwarn-only(无效果)\n真正支持:插入到 cache、从 cache 擦除\n\n\n缺失处理\n无缺失概念(全命中)\ncache miss → UVM table 透明回退\n\n\n智能插入\n无\nAutoInsertHandler 自动提升冷门键\n\n\n命中率报告\n固定 1.0\n真实命中率统计\n\n\n核心设计目标:当完整嵌入表无法完全放入 GPU 显存(但可以放入 UVM 内存),通过一个小型 GPU 高速缓存 + UVM 大容量后端的组合,既保证热数据的高速访问,又保证全量数据的可用性。\n\n配置结构体 LinearUVMEmbeddingLayer::Configstruct Config {\n std::string layer_name; // 层名称\n std::shared_ptr<InsertHeuristic> insert_heuristic; // 插入策略(nullptr = 默认)\n int64_t min_insert_freq_gpu = 0; // 两次 GPU 插入之间的最小查找次数\n int64_t min_insert_size_gpu = 1 << 16; // 触发 GPU 插入的最小键数\n};\n\n\ninsert_heuristic:决定何时触发自动插入。默认为 DefaultInsertHeuristic,阈值为命中率低于 0.75。可用 NeverInsertHeuristic 完全禁用自动插入\nmin_insert_freq_gpu:GPU 插入频率控制。设为 0 表示每次都评估是否插入,设为正值表示跳过前 N 次查找\nmin_insert_size_gpu:批处理阈值。缺页键先收集,达到 65536 个时才批量插入到 GPU 缓存,避免频繁的小批量插入\n\n\n底层架构:GpuTable + EmbedCacheSALinearUVMEmbeddingLayer 不直接操作 GPU 缓存——它委托给 GpuTable<KeyType> 对象。而 GpuTable 的核心是 EmbedCacheSA<KeyType, KeyType>——一个集合关联软件管理缓存。\nGpuTable 的构建// src/gpu_table.cu: 构造函数\ntemplate <typename KeyType>\nGpuTable<KeyType>::GpuTable(const GPUTableConfig& config, allocator_ptr_t allocator) {\n if (config.modify_on_gpu) {\n cache_ = std::make_shared<CacheSADeviceModify<KeyType, KeyType>>(...);\n } else {\n cache_ = std::make_shared<CacheSAHostModify<KeyType, KeyType>>(...);\n }\n cache_->init();\n}\n\n对于 LinearUVMEmbeddingLayer,关键在 GPUTableConfig 中的两个 UVM 相关字段:\n\nuvm_table:指向 UVM 线性内存的指针,这是全量嵌入表\ncount_misses:必须为 true,因为需要收集 miss 计数来驱动 AutoInsertHandler\n\nGPUTableConfig 完整字段struct GPUTableConfig {\n int device_id{0}; // GPU 设备编号\n size_t cache_size; // GPU 缓存总大小(字节)\n int64_t row_size_in_bytes; // 每行嵌入向量字节数\n void* uvm_table{nullptr}; // UVM 线性后备表指针\n bool count_misses{true}; // 是否收集 miss 计数\n int64_t max_modify_size{1 << 20}; // 单次修改操作的最大条目数\n DataType_t value_dtype; // 数据类型\n cudaStream_t private_stream{0}; // 私有 CUDA 流\n bool disable_uvm_update{false}; // 禁用 UVM 表更新\n bool uvm_cpu_accumulate{true}; // UVM 累积使用 CPU\n bool data_storage_on_host{false}; // 缓存数据存储在主机上\n bool modify_on_gpu{true}; // 修改操作在 GPU 上\n int64_t invalid_key{-1}; // 无效键值\n};\n\n\n查找路径的完整调用链LinearUVMEmbeddingLayer::lookup()\n │\n ├── ① ScopedDevice: 切换 GPU 设备\n │\n ├── ② BufferWrapper 包装 keys / output / offsets\n │\n ├── ③ 决定是否收集 misses:\n │ if (hitrates 非空 || insert_heuristic 不是 NeverInsertHeuristic)\n │ → 需要收集 miss → gpu_table_->reset_lookup_counter()\n │\n ├── ④ 分流:\n │ ├── 有 pool_params → 按 value_dtype 模板化分发\n │ │ ├── Float32 → gpu_table_->find_and_combine_bw<KeyType, float>(...)\n │ │ └── Float16 → gpu_table_->find_and_combine_bw<KeyType, __half>(...)\n │ └── 无 pool_params → gpu_table_->find_bw(...)\n │\n ├── ⑤ 如果 output 在 Host → cudaMemcpyAsync 回传\n │\n └── ⑥ 需要收集 miss?\n ├── gpu_table_->get_lookup_counter() 获取 miss 数\n ├── cudaStreamSynchronize(lookup_stream) 同步\n ├── 计算命中率 hitrate = 1 - misses/num_keys\n ├── 写入 hitrates 指针(如果需要)\n └── AutoInsertHandler::auto_insert() 自动插入\n\nGpuTable::find() 的 UVM 模式当 config_.uvm_table 不为空时,GpuTable::find() 进入 UVM 模式:\nif (config_.uvm_table) {\n std::unique_lock uvm_lock(uvm_table_mutex_, std::defer_lock);\n if (!config_.private_stream) {\n uvm_lock.lock();\n }\n run_find_uvm<KeyType, CacheType>(config_, cache_, ctx, num_keys, keys,\n values, value_stride, sc.queue_stream);\n} else {\n // 普通 cache-only 模式\n cache_->lookup(lookup_ctx, keys, num_keys, values, hit_mask, ...);\n}\n\nrun_find_uvm 的四种 kernel 模式run_find_uvm 通过 get_kernel_mode(config, num_keys) 选择执行策略:\n\n\n\n模式\n名称\n阈值\n机制\n\n\n\nLookupUVM\n直接 UVM 查找\nnum_keys < 1M\n一次 cache_->lookup 调用,UVM 硬件自动处理缺页\n\n\nSortGather\n排序-聚集\nnum_keys ≥ 1M\n先对 keys 排序去重,再用大块连续读取 UVM,按原始顺序 gather\n\n\nPipelineGather\n流水线聚集\n用户指定\n自定义流水线参数,分 tile 处理\n\n\nDynamicKernel\n动态\n—\n暂未实现\n\n\nLookupUVM 的内部机制:当 uvm_table 参数非空时,EmbedCacheSA::lookup 的内部 CUDA kernel 执行:\n每个线程处理一个 key:\n 1. 计算 hash(key) → set_id\n 2. 在 set 内逐一比较 tag\n 3. 如果 cache hit: 从 cache 数据区读取 → 写入 output\n 4. 如果 cache miss: \n a. 从 uvm_table + key * row_size 读取 → 写入 output\n b. 记录该 key 到 missing_keys 数组(用于计数和后续自动插入)\n\ncache miss 时读取 UVM 内存,由于 UVM 是 Unified Virtual Memory,GPU 硬件会自动触发缺页(page fault)→ 从 CPU 内存迁移页面到 GPU。这是实现透明回退的关键机制。\nfind_and_combine_bw(带 pooling 的查找)当有 pooling 参数时,LinearUVMEmbeddingLayer 调用 GpuTable::find_and_combine_bw(),使用 cuEmbed kernel 进行 GPU 端 pooling 聚合。类型组合的显式实例化:\n// float 数据\ntemplate void GpuTable<int64_t>::find_and_combine_bw<int64_t, float, float, float>(...);\ntemplate void GpuTable<int32_t>::find_and_combine_bw<int32_t, float, float, float>(...);\n// half 数据\ntemplate void GpuTable<int64_t>::find_and_combine_bw<int64_t, __half, __half, __half>(...);\ntemplate void GpuTable<int32_t>::find_and_combine_bw<int32_t, __half, __half, __half>(...);\n\n\nAutoInsertHandler 自动插入机制AutoInsertHandler(定义在 include/layer_utils.hpp)是 LinearUVMEmbeddingLayer 中最复杂的组件之一。\n运行流程auto_insert(layer_ctx, keys_bw, output_bw, hitrate, num_keys, output_stride)\n │\n ├── ① heuristic_->insert_needed(hitrate, 0) 评估是否需插入\n │ DefaultInsertHeuristic: hitrate < 0.75 → 需要\n │\n ├── ② 频率控制: min_insert_freq_gpu → insert_freq_cnt_++\n │ if (insert_freq_cnt_ < min_insert_freq_gpu) → 跳过\n │\n ├── ③ 收集缺页数据:\n │ collect_keys_and_data(keys_bw, output_bw, hitmask_bw, stream, num_keys)\n │ 从 hitmask 中提取 miss 的 key 和对应的 output 数据\n │ → 存入 insert_keys_ / insert_data_ 缓冲区\n │ → collected_keys_ += 缺页数\n │\n ├── ④ 批量检查:\n │ if (collected_keys_ >= min_insert_size_gpu) → 执行实际插入\n │ else → 返回(继续收集)\n │\n └── ⑤ launch_insert(layer_ctx, output_stride)\n gpu_table_->insert() → EmbedCacheSA::insert() 通过 CUDA kernel 写入 cache\n\n频率与批量的双重节流\n频率节流:min_insert_freq_gpu 确保两次插入之间至少间隔若干次查找,避免 GPU cache 被频繁扰动\n批量节流:min_insert_size_gpu(默认 65536)确保收集足够多的缺页键后才执行一次批量插入\n\nInsertHeuristic 策略class DefaultInsertHeuristic : public InsertHeuristic {\n static constexpr float DEFAULT_THRESHOLD = 0.75f;\n bool insert_needed(const float hitrate, const size_t table_id) override {\n return hitrate < thresholds_.at(table_id);\n }\n};\n\n当命中率低于 75% 时触发插入,表示当前 GPU 缓存的覆盖能力不足,需要补充热键。\n\ninsert() 操作的底层实现LinearUVMEmbeddingLayer::insert() 委托给 GpuTable::insert()。根据 modify_on_gpu 标志选择 GPU 或 CPU 路径。\nGPU 路径(modify_on_gpu = true):\n// 构建直方图(去重 + 聚合优先级)\nDefaultGPUHistogram<KeyType> histogram(num_keys);\nhistogram.compute_histogram(keys, num_keys, values, value_stride,\n d_hist_storage, mod_stream);\n\n// 调用集合关联缓存的 insert kernel\ncache_->insert(\n mod_ctx,\n histogram.get_keys(), // 去重后的 key\n histogram.get_priority(), // 优先级(基于出现频率)\n histogram.get_data(), // 嵌入数据\n histogram.get_num_bins(), // 去重后的条目数\n 0, ec_event.get(), sc.queue_stream);\n\n关键点:\n\n直方图去重:DefaultGPUHistogram 对输入 keys 进行 GPU 端直方图统计,按出现频率排序——高频键获得更高优先级,类似 LFU 策略\n集合关联 insert:EmbedCacheSA::insert 使用 LRU/LFU 淘汰策略,优先保留高频访问的嵌入向量\nmodify_context:管理修改操作的临时缓冲区,大小由 max_modify_size(默认 1M)控制\n\n\nupdate() 操作的底层实现if (auto_insert_handler_) {\n auto_insert_handler_->lock_modify(); // 阻止自动插入在修改期间执行\n}\ngpu_table_->update_bw(...);\nif (auto_insert_handler_) {\n auto_insert_handler_->unlock_modify();\n}\n\nGpuTable::update() 的双重更新GpuTable::update() 执行两处更新:\n\n先更新 GPU 缓存:通过 EmbedCacheSA::update() kernel 覆盖缓存中已有键的值,分批处理,每批大小不超过 max_modify_size\n\n再更新 UVM 表:如果 disable_uvm_update 为 false,使用 UpdateTable kernel 更新 UVM 内存中的全量表\n\n\nif (config_.uvm_table && !config_.disable_uvm_update) {\n sync = contexts_->create_sync_event();\n sync->event_record();\n sync->event_wait_stream(update_stream);\n StreamCoordinator::create_stream_dependency(modify_stream, update_stream);\n UpdateTable<KeyType>(values, d_keys, config_.uvm_table, ...);\n cudaEventRecord(uvm_update_event, update_stream);\n cudaStreamWaitEvent(modify_stream, uvm_update_event);\n}\n\n这种”双重写入”确保了 GPU 缓存和 UVM 后备表的一致性:后续 cache miss 从 UVM 读取到的数据始终是最新的。\n\naccumulate() 梯度累积的 CPU 路径当 config_.uvm_cpu_accumulate = true 且需要同步 UVM 表时,GpuTable::update_accumulate() 采用 CPU 累积路径:\nStep 1: cudaMemcpyAsync 分批将梯度数据从 GPU 传回 Host\n └── 每批 512 个键,每批记录一个 cudaEvent\nStep 2: 提交 meta task 到线程池\nStep 3: meta task 等待 cudaEventSynchronize(每批的拷贝完成)\nStep 4: 每批再细分成 tasks_per_copy 个子任务,提交到线程池\n └── 每个子任务处理 keys_per_task 个键\n └── 在 CPU 上进行循环累加:\n for (j = 0; j < elements_per_row; j++)\n dst[key][j] += gradient[j]\nStep 5: 等待所有子任务完成(std::promise + future 同步)\nStep 6: 销毁拷贝事件\n\n为什么不用 GPU kernel 做 UVM accumulate?\nCPU 累积的优势在于避免 UVM page fault 的连锁代价。如果直接在 GPU 上以 atomicAdd 更新 UVM 内存,大量稀疏随机写入会导致严重的缺页抖动——每写一个不同的页面都可能触发 page fault。\n小规模查找使用简单路径(直接 cudaMemcpyAsync + CPU 循环),大规模查找使用上述流水线多拷贝路径。\n\nerase() 与 clear()erase() 调用 GpuTable::erase(),最终通过 EmbedCacheSA::invalidate kernel 清除缓存中的 tag:\ncache_->invalidate(mod_ctx, keys, num_keys, 0, ec_event.get(), sc.queue_stream);\n\nclear() 调用 EmbedCacheSA::clear_cache:\ncache_->clear_cache(sc.queue_stream);\n\n\nGPUTableExecutionContext 的缓存上下文管理GPUTableExecutionContext 是 GpuTable 的执行上下文,它持有关键资源的句柄:\nclass GPUTableExecutionContext : public ExecutionContext {\n LookupContextHandle lookup_context_; // 查找操作的 cache 上下文\n ModifyContextHandle modify_context_; // 修改操作的 cache 上下文\n PerformanceMetric miss_metric_; // miss 计数指标\n cache_ptr_type cache_; // EmbedCacheSA 实例\n};\n\n\nlookup_context_:由 cache_->lookup_context_create() 创建,存储查找操作所需的临时状态\nmodify_context_:由 cache_->modify_context_create(max_modify_size) 创建,包含修改操作的批量缓冲区\nmiss_metric_:当 count_misses = true 时创建,在每次 lookup 后通过 get_lookup_counter() 读取 miss 计数\n构造时注册到 ContextRegistry,析构时注销(与 GPUEmbeddingLayer 的模式一致)\n\n\nUVM 表与 GPU 缓存的互斥保护UVM 表是一个共享资源——多个 GpuTable 实例可能共享同一个 UVM 表。uvm_table_mutex_ 确保 UVM 表更新的串行化:\nstd::unique_lock uvm_lock(uvm_table_mutex_, std::defer_lock);\nif (!config_.private_stream) {\n uvm_lock.lock();\n}\n\n使用 private_stream 时,流本身提供了串行化保证;否则通过互斥锁保护 UVM 表的写入。\n\n使用示例C++ API 使用// 1. 创建 UVM 后备表 (完整嵌入表)\nsize_t num_embeddings = 100000000;\nsize_t embed_width = 128; // 128 字节 (32 个 float)\nvoid* uvm_table;\ncudaMallocManaged(&uvm_table, num_embeddings * embed_width);\n\n// 2. 配置 GPU 缓存\nGPUTableConfig cache_config;\ncache_config.device_id = 0;\ncache_config.cache_size = 2ULL * 1024 * 1024 * 1024; // 2GB GPU 缓存\ncache_config.row_size_in_bytes = embed_width;\ncache_config.uvm_table = uvm_table;\ncache_config.count_misses = true;\ncache_config.value_dtype = DataType_t::Float32;\n\n// 3. 创建 GpuTable 和 LinearUVMEmbeddingLayer\nauto gpu_table = std::make_shared<GpuTable<int64_t>>(cache_config);\n\nLinearUVMEmbeddingLayer<int64_t>::Config layer_config;\nlayer_config.layer_name = \"my_embedding\";\n\nLinearUVMEmbeddingLayer<int64_t> layer(layer_config, gpu_table);\n\n// 4. 创建执行上下文\nauto ctx = layer.create_execution_context(\n lookup_stream, modify_stream, thread_pool, allocator);\n\n// 5. 执行查找\nint64_t num_keys = 4096;\nint64_t keys[num_keys];\nfloat output[num_keys * 32];\n\nlayer.lookup(ctx, num_keys, keys, output, embed_width,\n nullptr, nullptr, nullptr);\n\n// 6. 更新\nlayer.update(ctx, num_keys, keys, embed_width, embed_width, new_values);\n\nPython API 使用import pynve\n\n# 创建 LinearUVM 嵌入层\nembedding = pynve.NVEmbedding(\n num_embeddings=100_000_000,\n embedding_dim=128,\n cache_type=\"LinearUVM\",\n cache_size=2_000_000_000, # 2GB GPU 缓存\n)\n\n# 前向查找\nx = torch.tensor([1, 2, 3, 4, 5], device='cuda')\noutput = embedding(x)\n\n\n性能特征与调优建议查找延迟\n\n\n场景\n相对延迟\n说明\n\n\n\nCache hit\n低\nGPU cache 标签匹配 + 数据读取,纯 GPU kernel 延迟\n\n\nCache miss(小量)\n中-高\nUVM page fault,GPU 从 CPU 内存迁移页面\n\n\nCache miss(大量)\n取决于 UVM 带宽\nSortGather 模式通过排序提升 UVM 读取效率\n\n\n关键调优参数\n\n\n参数\n推荐值\n影响\n\n\n\ncache_size\nGPU 显存的 10-50%\n越大命中率越高,但留给其他操作的空间越少\n\n\nmin_insert_size_gpu\n1<<16 ~ 1<<20\n越小插入越频繁,越大 batch 效率越高\n\n\nmin_insert_freq_gpu\n0~100\n越大插入频率越低,缓存越稳定\n\n\nkernel_mode_type\n0(自动)\n小 batch 用 LookupUVM,大 batch 自动切换 SortGather\n\n\nuvm_cpu_accumulate\ntrue\nCPU 累积避免 GPU UVM 页错误\n\n\nUVM 的陷阱与应对\nUVM page fault 叠加:大量随机读取 miss 可能导致大量并发缺页,严重降低性能。SortGather 模式通过排序来改善局部性\nGPU 缓存大小与命中率的折衷:cache 越大命中率越高,但占用显存也越多。建议通过基准测试找到最佳点\ndouble write 开销:update() 同时写 GPU cache 和 UVM 表,带来约 2 倍的写带宽消耗。disable_uvm_update 可禁用 UVM 写入\n\n\n与 HierarchicalEmbeddingLayer 的对比\n\n\n维度\nLinearUVMEmbeddingLayer\nHierarchicalEmbeddingLayer\n\n\n\n第二级存储\nUVM 线性内存(虚拟统一内存)\nCPU 哈希表 + 远程表\n\n\n二级访问方式\nGPU 直接读取(硬件缺页)\nCPU 线程池并行查找\n\n\nhost 回退\n无(全程 GPU)\n有(host + remote)\n\n\n数据持久化\n否(UVM 内存不持久)\n是(RocksDB/Redis)\n\n\n适用嵌入大小\n可超出显存但不超过系统内存\n远超系统内存(数 TB)\n\n\n配置复杂度\n低(只需 UVM table pointer)\n高(host table 配置 + 远程连接)\n\n\n简而言之:LinearUVMEmbeddingLayer 是 GPUEmbeddingLayer 到 HierarchicalEmbeddingLayer 之间的中间方案——牺牲少量查找延迟(miss 时 UVM 缺页代价),换取更大的存储容量,同时保持全程 GPU 端执行、无需 CPU 回退的优势。\n","categories":["inference"],"tags":["NVIDIA","Embedding","GPU","CUDA","NVEmbeddingCache","UVM"]},{"title":"multi-gpu-deep","url":"/posts/96878a9b.html","content":"多 GPU 与分布式部署深度分析概述当单个 GPU 的显存无法容纳完整嵌入表,或需要在多块 GPU / 多节点上部署相同模型时,NVE 提供了多种资源共享和分片方案。本文档从原理到代码实现,详细分析每种方案的设计和用法。\n核心理念:嵌入层之间不共享 GPU 缓存;共享仅限于更大的后备存储资源。\n\n一、共享场景分类场景 1:单节点多 GPU 推理同一节点部署同一模型的多个副本(每个 GPU 一个),各副本读取相同嵌入数据。共享方案:\n\n\n\n嵌入层类型\nC++\nPython\n共享方式\n\n\n\nGPUEmbeddingLayer\n同一 embedding_table 指针\n不支持(每个 NVEmbedding 独立权重)\n指向同一 GPU/Host 缓冲区\n\n\nLinearUVMEmbeddingLayer\n同一 uvm_table 指针\n同一 ManagedMemBlock\nUVM 后备表在 Host 内存或跨 GPU 映射\n\n\nHierarchicalEmbeddingLayer\n同一 host table / remote PS\n暂不支持 host table 共享\n参数服务器天然共享\n\n\n场景 2:单节点多 GPU 分片将一个大嵌入表按行分片到多个 GPU,每个 GPU 拥有一部分物理内存,但所有分片映射到统一虚拟地址空间。\n场景 3:多节点分片跨节点共享嵌入表,通过 IMEX(Inter-Node Memory Extension)通道 + NVLink Fabric 实现。\n\n二、GPUEmbeddingLayer — 指针共享GPUEmbeddingLayer 从 config.embedding_table 获取用户传入的指针,不拥有该内存的所有权:\nGPUTableConfig config;\nconfig.embedding_table = shared_buffer; // 同一指针传入多个 layer\n\n多个 GPUEmbeddingLayer 实例使用同一 embedding_table 指针即可共享数据。\n更好的方式:使用同一 GPUEmbeddingLayer 的不同执行上下文(ExecutionContext),而不是多个层实例:\n// 单个层 + 多个上下文\nauto layer = std::make_shared<GPUEmbeddingLayer<int64_t>>(config);\nauto ctx1 = layer->create_execution_context(stream1, ...);\nauto ctx2 = layer->create_execution_context(stream2, ...);\n\n// 线程 1 使用 ctx1,线程 2 使用 ctx2\npool->submit([&]() { layer->lookup(ctx1, ...); });\npool->submit([&]() { layer->lookup(ctx2, ...); });\n\n共享 GPU 嵌入表的注意事项:\n\n所有 GPU kernel 通过 ScopedDevice 切换到目标设备后才能访问指针\n如果指针指向的是另一个 GPU 的内存,需要 GPU 支持统一地址访问(NVLink 连接)\n写入时(update() / accumulate())需要应用层处理读后写冲突\n\n\n三、LinearUVMEmbeddingLayer — UVM 表共享这是最常用的多 GPU 共享方案。LinearUVMEmbeddingLayer 的 GPU cache 是私有的,但 UVM 后备表可以共享。\nHost 内存共享(单节点)// 1. 分配 UVM 后备表(首选 Host 位置)\nvoid* linear_table = nullptr;\ncudaMallocManaged(&linear_table, linear_table_size);\n\n// 2. 优化内存访问:建议所有 GPU 访问,首选位置为 CPU\ncudaMemAdvise(linear_table, linear_table_size,\n cudaMemAdviseSetAccessedBy, 0); // GPU 0\ncudaMemAdvise(linear_table, linear_table_size,\n cudaMemAdviseSetAccessedBy, 1); // GPU 1\ncudaMemAdvise(linear_table, linear_table_size,\n cudaMemAdviseSetPreferredLocation, cudaCpuDeviceId);\n\n// 3. 为每块 GPU 创建 GpuTable(独立的 GPU cache)\nGPUTableConfig cfg;\ncfg.device_id = 0;\ncfg.cache_size = 1ULL << 31; // 2GB cache per GPU\ncfg.uvm_table = linear_table; // ← 共享指针\nauto table0 = std::make_shared<GpuTable<int64_t>>(cfg);\n\ncfg.device_id = 1;\nauto table1 = std::make_shared<GpuTable<int64_t>>(cfg);\n\n// 4. 创建独立的嵌入层\nLinearUVMEmbeddingLayer<int64_t> layer0({}, table0);\nLinearUVMEmbeddingLayer<int64_t> layer1({}, table1);\n\n原理:UVM 后备表分配在 Host 内存(cudaMallocManaged + cudaMemAdviseSetPreferredLocation = cudaCpuDeviceId),各 GPU 通过 UVM page fault 按需获取页面。每个 GpuTable 管理自己的 GPU cache,cache miss 时从同一块 Host 后备表读取。\n更新冲突:当多个层同时写入 UVM 表时,通过 disable_uvm_update 控制——只允许一个 GpuTable 负责 UVM 写入,其余 GPU 仅读取:\ncfg0.disable_uvm_update = false; // GPU 0 负责写入\ncfg1.disable_uvm_update = true; // GPU 1 只读\n\nPython ManagedMemBlockimport pynve.nve as nve\n\n# 分配 Host 内存块\nmemblock = nve.ManagedMemBlock(embedding_dim, num_table_rows, data_type)\n\n# 多个 NVEmbedding 共享同一 memblock\nemb0 = nve.NVEmbedding(num_table_rows, embedding_dim, data_type,\n cache_type=CacheType.LinearUVM, gpu_cache_size=cache_size,\n memblock=memblock, device=torch.device(\"cuda:0\"))\n\nemb1 = nve.NVEmbedding(num_table_rows, embedding_dim, data_type,\n cache_type=CacheType.LinearUVM, gpu_cache_size=cache_size,\n memblock=memblock, device=torch.device(\"cuda:1\"))\n\nManagedMemBlock 内部调用 cudaMallocManaged,在 Python 端暴露为线性内存块。\n\n四、CUDADistributedBuffer — CUDA 虚拟内存管理核心概念CUDADistributedBuffer(include/distributed.hpp)使用 CUDA Virtual Memory Management API 创建跨 GPU / 跨节点的统一虚拟地址映射缓冲区。\n与 UVM 的关键区别:\n\n\n\n维度\nUVM (cudaMallocManaged)\nCUDADistributedBuffer\n\n\n\n物理分配\nGPU 自动管理\n每个 GPU 显式分配物理内存\n\n\n地址映射\n统一虚拟地址 + 硬件缺页\n虚拟地址 + 显式映射\n\n\n性能\npage fault 有额外开销\n直接物理内存访问,无缺页\n\n\n跨节点\n不支持\n支持(通过 Fabric 句柄)\n\n\n适用场景\n单节点 Host 内存共享\n多节点 NVLink 分片\n\n\n构造函数流程CUDADistributedBuffer::CUDADistributedBuffer(\n uint64_t size,\n std::shared_ptr<DistributedEnv> dist_env,\n BufferLocation location) {\n if (single_host_) {\n init_single_host(size, location);\n } else {\n init_multi_host(size);\n }\n env_->barrier(); // 全部初始化完成后同步\n}\n\n单节点初始化(init_single_host)Rank 0 (root)\n │\n ├── collect_devices(): all_gather 收集各 rank 的设备 ID\n │\n ├── 计算 shard_size: 对齐到 num_shards × granularity\n │\n ├── 对每个有 GPU 的 rank:\n │ cuMemCreate(&handle, shard_size, ..., device_id)\n │ cuMemExportToShareableHandle(&fd, handle, POSIX_FILE_DESCRIPTOR)\n │ all_alloc_handles_.push_back(handle)\n │\n ├── broadcast(shareable_fds, root=0)\n │ broadcast(root_pid, root=0)\n │\n └── 非 root 进程:\n pidfd_open(root_pid) → pidfd_getfd() → 获取本地 FD\n │\n └── 所有进程:\n cuMemAddressReserve(&buffer_, total_size_, ...) // 预留虚拟地址\n cuMemImportFromShareableHandle(&handle, fd, ...) // 导入句柄\n cuMemMap(buf_start, shard_size_, ..., handle, ...) // 映射\n cuMemRelease(handle)\n │\n └── cuMemSetAccess(buffer_, total_size_, access_descs, ...)\n // 设置所有 GPU 的读写权限\n\n多节点初始化(init_multi_host)每个节点(rank)\n │\n ├── check_imex(): 检查 /dev/nvidia-caps-imex-channels 是否存在\n │ IMEX = Inter-Node Memory Extension\n │\n ├── cuMemCreate(&handle, shard_size_, ..., device_id)\n │ 物理内存分配在本地 GPU\n │\n ├── cuMemExportToShareableHandle(&fabric_handle, handle, FABRIC)\n │ 导出为 Fabric 句柄(跨节点共享)\n │\n ├── all_gather: 收集所有 rank 的 Fabric 句柄\n │\n ├── cuMemImportFromShareableHandle() → 导入各 rank 的句柄\n │\n ├── cuMemAddressReserve(&buffer_, total_size_, ...)\n │\n ├── cuMemMap() → 将每个分片映射到虚拟地址的对应位置\n │\n └── cuMemSetAccess() → 设置本地 GPU 访问权限\n\nIMEX 通道 是 NVIDIA 多节点 NVLink 系统的核心组件,为跨节点 GPU 间通信提供硬件路由能力。\n析构函数CUDADistributedBuffer::~CUDADistributedBuffer() {\n env_->barrier(); // 确保所有进程就绪\n for (auto& h : all_alloc_handles_) {\n cuMemUnmap(buf_start, shard_size_); // 取消映射\n cuMemRelease(h); // 释放物理分配\n }\n env_->barrier(); // 确保所有进程取消映射完毕\n cuMemAddressFree(buffer_, total_size_); // 释放虚拟地址空间\n}\n\n\n五、MPI 分布式环境DistributedEnv 接口class DistributedEnv {\npublic:\n virtual size_t rank() const = 0;\n virtual size_t world_size() const = 0;\n virtual size_t device_count() const = 0;\n virtual int local_device() const = 0;\n virtual bool single_host() const = 0;\n virtual void barrier() = 0;\n virtual void broadcast(uintptr_t buf, size_t size, int root) = 0;\n virtual void all_gather(uintptr_t send, uintptr_t recv, size_t size) = 0;\n};\n\nlocal_device() 是关键方法:它确定当前进程应该使用哪块 GPU。对于单节点多进程场景,通常 local_device = rank % device_count()。\ncollect_devices()uint64_t CUDADistributedBuffer::collect_devices(std::vector<int>& all_devices) {\n int local_device = env_->local_device();\n const auto world_size = env_->world_size();\n all_devices.resize(world_size);\n env_->all_gather(reinterpret_cast<uintptr_t>(&local_device),\n reinterpret_cast<uintptr_t>(&all_devices[0]),\n sizeof(local_device));\n uint64_t num_devices = 0;\n for (auto& d : all_devices) if (d >= 0) num_devices++;\n return num_devices;\n}\n\n\n六、Python 端的 MemBlock 系列类层次MemBlock (抽象基类, C++ binding)\n ├── LinearMemBlock — cudaMallocHost 分配的 Host 固定内存\n ├── ManagedMemBlock — cudaMallocManaged 分配的 UVM 内存\n ├── NVLMemBlock — 单节点多 GPU NVLink 共享内存\n ├── MPIMemBlock — 多节点 MPI 分布式共享内存\n └── DistMemBlock — 分布式环境共享内存(别名,同 MPIMemBlock)\n\nMPIMemBlock(Python)import pynve.nve as nve\nfrom mpi4py import MPI\n\ncomm = MPI.COMM_WORLD\nmpi_rank = comm.Get_rank()\nlocal_device_id = mpi_rank % torch.cuda.device_count()\n\n# 分配跨节点共享的分布式内存块\nmemblock = nve.MPIMemBlock(embedding_dim, num_table_rows, data_type)\n\n# 创建嵌入层(每个进程独立 GPU cache,共享后备表)\nemb_layer = nve_layers.NVEmbedding(\n num_table_rows, embedding_dim, data_type,\n cache_type=CacheType.LinearUVM,\n gpu_cache_size=cache_size,\n memblock=memblock,\n device=torch.device(f\"cuda:{local_device_id}\"))\n\nMPIMemBlock 内部绑定到 CUDADistributedBuffer,通过 MPI 通信交换 GPU 物理内存句柄。\nNVLMemBlock(Python)单节点多 GPU 场景下的 NVLink 共享内存:\n# 在 GPU 0,1,2 上分配物理内存分片\nmemblock = nve.NVLMemBlock(embedding_dim, num_table_rows,\n data_type, gpu_ids=[0, 1, 2])\n\nNVLMemBlock 内部在指定的每块 GPU 上分配物理内存,通过 cuMemCreate / cuMemExportToShareableHandle / cuMemImportFromShareableHandle 将各分片映射到统一虚拟地址空间(与 init_single_host 逻辑相同)。\n两种 NoCache 路径对于 CacheType.NoCache,Python 端使用 LinearMemBlock(Host 固定内存)或 ManagedMemBlock(UVM 内存),嵌入向量直接存储在其中,没有 GPU cache 层。查找时通过 cuEmbed kernel 直接读取。\n\n七、数据竞争与读写安全只读场景当嵌入表只读时(推理场景),不需要额外同步。每个 GpuTable 独立管理自己的 GPU cache,从共享后备表读取数据。cache 行通过 UVM 或 NVLink 读取。\n写入场景当共享资源被修改时,其他 layer 必须被阻塞直到修改完成:\nLayer 0 (GPU 0) Layer 1 (GPU 1)\n │ │\n ├ update(keys, values) │\n │ ↓ │\n │ UpdateTable kernel │\n │ (写入 UVM 后备表) │\n │ ↓ │\n │ cudaEventRecord │\n │ ↓ │\n │ cudaStreamSynchronize │\n │ ├ lookup(keys) → (等待完成)\n │ │ ↓\n │ │ GpuTable::find()\n │ │ (读取 UVM 后备表)\n\n同步方式:\n C++: cudaEvent + cudaStreamWaitEvent (GPU 级异步)\n Python: torch.cuda.synchronize() / MPI barrier()\n\nCache 一致性问题如果直接修改共享后备表(绕过 layer->update()),GPU cache 中可能还持有旧值:\n// 方案 1: 调用 update() 更新缓存和后备表\nlayer->update(ctx, num_keys, keys, stride, size, new_values);\n\n// 方案 2: 调用 erase() 淘汰缓存条目(下次 lookup 时自动从后备表加载)\nlayer->erase(ctx, num_keys, keys, table_id);\n\n\n八、性能特征对比方案总结\n\n\n方案\nC++ 实现\nPython 实现\n跨节点\n性能特征\n\n\n\nHost 内存共享\ncudaMallocHost + 多 GpuTable\nManagedMemBlock\n❌\n延迟高(UVM page fault)\n\n\nUVM 共享\ncudaMallocManaged + cudaMemAdvise\nManagedMemBlock\n❌\n延迟中等(UVM 自动迁移)\n\n\nNVLink 分片\nCUDADistributedBuffer (single_host)\nNVLMemBlock\n❌\n延迟低(硬件直连)\n\n\n多节点分片\nCUDADistributedBuffer (multi_host)\nMPIMemBlock\n✅\n延迟由 NVLink Fabric 决定\n\n\n多 GPU 基准测试项目在 benchmarks/multi_gpu_bench.py 中提供了完整的分布式基准测试框架,使用 torch.distributed + mpi4py 启动多进程训练/推理:\n# 单节点 4 GPU\nmpirun -n 4 python benchmarks/multi_gpu_bench.py \\\n --memblock managed\n\n# 跨节点(需要 IMEX 通道)\nmpirun -n 8 --host node1:4,node2:4 \\\n python benchmarks/multi_gpu_bench.py \\\n --memblock mpi\n\n\n九、术语表(GLOSSARY)\n\n\n术语\n含义\n\n\n\nUVM\nCUDA Unified Virtual Memory。统一虚拟地址空间,可同时映射到 GPU 和 Host 物理内存。\n\n\nNVLink\nNVIDIA 的高带宽 GPU 直连技术,支持多 GPU 间低延迟通信。\n\n\nNVSwitch\nNVLink 交换机,允许多 GPU 全互联。\n\n\nIMEX\nInter-Node Memory Extension。跨节点 NVLink 通道,使多节点 GPU 能够直接访问对方物理内存。\n\n\nFabric Handle\nCUDA 虚拟内存管理的跨节点共享句柄类型(CU_MEM_HANDLE_TYPE_FABRIC)。\n\n\nDDP\nDistributed Data Parallel。数据并行训练策略,每个 GPU 持有完整模型副本但处理不同数据分片。\n\n\nSharding\n将嵌入表按行分割到多个 GPU/节点,每个 GPU 只持有部分物理内存。\n\n\npage fault\n当 GPU 访问尚未映射到本地显存的 UVM 页面时触发的缺页中断。\n\n\npidfd\nLinux 进程文件描述符(pidfd_open + pidfd_getfd),允许跨进程共享文件句柄。\n\n\n","categories":["inference"],"tags":["NVIDIA","Embedding","CUDA","NVEmbeddingCache","UVM","Multi-GPU","Distributed","NVLink","MPI"]},{"title":"plugin-system","url":"/posts/c7d29735.html","content":"NVE 插件系统一、总体架构NVE 的插件系统是一套 C 符号约定 + 抽象工厂 + 动态加载 的架构,用于提供多种宿主表(HostTable)存储后端。整体分三层:\n ┌──────────────────────────────────┐\n │ build_host_database() │ ← 顶层编排函数\n └────┬─────────────┬───────────────┘\n │ │\n┌────────────▼──┐ ┌──────▼──────────────┐\n│ load_plugin()│ │ resolve_factory() │\n│ dlopen .so │ │ htf_impls[] map │\n└────┬──────────┘ └──────┬──────────────┘\n │ │\n┌────▼──────────────────────▼──────────────────┐\n│ 静态注册的内置实现 │\n│ "stl_map" / "map" / "umap" / "unordered_map"│\n│ → STLMapTableFactory │\n└───────────────────────────────────────────────┘\n\n\n二、插件接口定义插件 API 是一组 C 链接函数指针类型定义,而非 C++ 虚基类:\nnamespace nve {\n\nusing plugin_info_t = const char* (*)(); // 插件元信息\nusing enum_implementations_t = void (*)(void*, void (*)(void*, const char*)); // 枚举实现\nusing create_host_table_factory_t = host_table_factory_ptr_t (*)(const nlohmann::json&); // 工厂创建\n\n} // namespace nve\n\n每个插件 .so 必须导出以下 3 个 C 链接函数:\n\n\n\n导出的 C 符号\n签名\n作用\n\n\n\nplugin_ident()\nextern "C" const char*()\n返回插件人类可读名称\n\n\nplugin_developer()\nextern "C" const char*()\n返回开发者名称\n\n\nenum_host_table_implementations()\nextern "C" void(void*, void(*)(void*,const char*))\n遍历注册插件内所有实现\n\n\ncreate_{name}_table_factory()\nextern "C" host_table_factory_ptr_t(const nlohmann::json&)\n每个实现一个,创建工厂对象\n\n\n\n这不是 C++ 虚基类,而是纯 C 符号约定——任何遵守这个符号约定的 .so 都可以被 dlopen + dlsym 动态加载。\n\n\n三、运行时加载机制加载流程在 src/host_table.cpp 中实现,共 4 步:\nStep 1 — dlopen 加载 .sovoid load_host_table_plugin(const std::string_view& plugin_name) {\n void* dll = dlopen(plugin_path.c_str(), RTLD_NOW | RTLD_GLOBAL);\n // 如果给定路径失败,回退到相对于当前 .so 的路径\n}\n\nStep 2 — dlsym 解析 3 个必需符号plugin_info_t plugin_ident = dlsym(dll, \"plugin_ident\");\nplugin_info_t plugin_dev = dlsym(dll, \"plugin_developer\");\nenum_implementations_t enum_ht = dlsym(dll, \"enum_host_table_implementations\");\n\nStep 3 — 枚举注册实现enum_ht(dll, register_implementation);\n\nregister_implementation 回调函数构造 "create_{name}_table_factory" 字符串,再次 dlsym 解析工厂构造函数,存入全局 std::unordered_map<string, create_host_table_factory_t>:\nstatic std::unordered_map<std::string, create_host_table_factory_t> htf_impls;\n\nStep 4 — 按 JSON 配置生产表host_table_factory_ptr_t create_host_table_factory(const nlohmann::json& json);\nhost_table_ptr_t factory->produce(table_id_t id, const nlohmann::json& config);\n\n\n四、类继承体系┌──────────────────────────────────────────────────────────────┐\n│ nve::Table (abstract) │\n│ virtual clear() / erase() / find() / insert() / update() │\n└──────────────────────────────────────────────────────────────┘\n ▲\n ┌──────────────────┴──────────────────┐\n │ │\n┌───────┴─────────────────────────┐ ┌────────┴────────────────┐\n│ nve::HostTableLike │ │ GPU 端表 (抽象) │\n│ + size() / config() 等 │ │ (gpu_table.hpp) │\n└──────────────────────────────────┘ └─────────────────────────┘\n ▲\n┌───────┴─────────────────────────┐\n│ nve::HostTable<ConfigType> │ ← CRTP 基类,存储 config\n└──────────────────────────────────┘\n ▲\n ├───────────────────────────────────┬───────────────────┐\n │ │ │\n┌───────┴──────────┐ ┌─────────────────────┴──┐ ┌─────────────┴──────────┐\n│ STLContainerTable│ │ NvhmMapTable │ │ RedisClusterTable │\n│ (umap / map) │ │ (GPU nvhashmap 后端) │ │ (远程 Redis 集群) │\n└──────────────────┘ └────────────────────────┘ └────────────────────────┘\n ▲ ▲\n AbseilFlatMapTable │\n PHMapFlatMapTable │\n (继承 STLContainerTable) │\n │\n ┌────────┴──────────┐\n │ RocksDBTable │\n │ (磁盘 LSM-Tree) │\n └───────────────────┘\n\n\n关键区别:NVHM 插件直接继承 HostTable<NvhmMapTableConfig>,不走 STL 层。Abseil 和 PHMAP 继承 STLContainerTable,复用其分片、超额淘汰、内存管理等基础设施。Redis 和 RocksDB 各自独立继承 HostTable,因为它们操作的是远程网络连接或本地磁盘。\n\n工厂类层次HostTableLikeFactory (抽象工厂基类)\n └── HostTableFactory<FactoryConfig, TableConfig> (模板工厂)\n ├── STLMapTableFactory → 内置(非插件)\n ├── NvhmMapTableFactory → NVHM 插件\n ├── AbseilFlatMapTableFactory → Abseil 插件\n ├── PHMapFlatMapTableFactory → PHMAP 插件\n ├── RedisClusterTableFactory → Redis 插件\n └── RocksDBTableFactory → RocksDB 插件\n\n\n五、5 个插件详解5.1 NVHM 插件(libnve-plugin-nvhm.so)\n\n\n属性\n内容\n\n\n\n注册名\nnvhm_map\n\n\n底层库\nNVIDIA nvhashmap — GPU 端开放寻址哈希表\n\n\n存储位置\nGPU device memory\n\n\n数据持久化\n❌ 不持久\n\n\n核心配置:\n\n\n\n字段\n默认值\n说明\n\n\n\nnum_partitions\n1\n独立分片数(必须为 2 的幂)\n\n\nkernel_size\nnvhashmap 默认\n内核大小 (1-1024)\n\n\nkey_fetch_queue_length\n8\n软件预取管道长度 {0,1,2,4,8}\n\n\nprefetch_values\ntrue\n是否发出软件预取指令\n\n\nminimize_psl\nfalse\n是否缩短 probe search length\n\n\nauto_shrink\nfalse\n大量淘汰后是否自动缩容\n\n\noverflow_policy\n—\n淘汰策略(EvictRandom / EvictLRU / EvictLFU)\n\n\n模板分发:produce() 在多维 switch 上分发:mask_size × key_size × overflow_handler × kernel_size × minimize_psl × auto_shrink × partitioner → ~6144 种编译时类型组合。\n\n5.2 Abseil 插件(libnve-plugin-abseil.so)\n\n\n属性\n内容\n\n\n\n注册名\nabseil_flat_map\n\n\n底层库\nGoogle absl::flat_hash_map<KeyType, char*>(Swiss Table)\n\n\n存储位置\nCPU 端内存\n\n\n数据持久化\n❌ 不持久\n\n\n实现方式:继承 STLContainerTable,复用全部分片、内存槽管理、超额淘汰逻辑,底层哈希表替换为 absl::flat_hash_map。\n链接依赖:absl_raw_logging_internal + absl_hash + absl_raw_hash_set。\n\n5.3 PHMAP 插件(libnve-plugin-phmap.so)\n\n\n属性\n内容\n\n\n\n注册名\nphmap_flat_map\n\n\n底层库\ngreg7mdp parallel-hashmap phmap::flat_hash_map\n\n\n存储位置\nCPU 端内存\n\n\n数据持久化\n❌ 不持久\n\n\n实现方式:与 Abseil 插件完全对称——继承 STLContainerTable,底层替换为 phmap::flat_hash_map。模板分发维度:mask_size × key_size × overflow_handler × partitioner → ~192 种类型组合。\n\n5.4 Redis 插件(libnve-plugin-redis.so)\n\n\n属性\n内容\n\n\n\n注册名\nredis_cluster\n\n\n底层库\nhiredis + redis++ — TCP 连接 Redis Cluster\n\n\n存储位置\n远程 Redis 集群\n\n\n数据持久化\n✅ 持久化\n\n\n核心配置:\n\n\n\n字段\n默认值\n说明\n\n\n\naddress\nlocalhost:6379\nRedis 集群任意节点地址\n\n\nuser_name\ndefault\nRedis 用户名\n\n\npassword\n空\nRedis 用户密码\n\n\nconnections_per_node\n5\n与每个 Redis 节点的最大并行连接数\n\n\nconnection_lifetime\n180 秒\n连接生命周期\n\n\nmax_batch_size\n16384\n批量查询的最大 batch size\n\n\n链接依赖:hiredis + hiredis_ssl + redis++_static + ssl + crypto。\n\n5.5 RocksDB 插件(libnve-plugin-rocksdb.so)\n\n\n属性\n内容\n\n\n\n注册名\nrocksdb\n\n\n底层库\nFacebook RocksDB(LSM-Tree)\n\n\n存储位置\n本地磁盘\n\n\n数据持久化\n✅ 持久化\n\n\n核心配置:\n\n\n\n字段\n默认值\n说明\n\n\n\ncolumn_family\ndefault\nRocksDB Column Family 名称\n\n\nmax_batch_size\n16384\n批量操作的最大 batch size\n\n\nverify_checksums\ntrue\n是否校验 checksum\n\n\npath\n/tmp/rocksdb\n数据库文件系统路径\n\n\nread_only\nfalse\n只读模式(允许多客户端并发读取)\n\n\nnum_threads\n16\nRocksDB 实例可用线程数\n\n\n\n六、内置降级实现(无插件)即使不加载任何插件,也有 6 个别名指向同一个内置实现:\nstatic std::unordered_map<std::string, create_host_table_factory_t> htf_impls{\n {\"stl_map\", create_stl_map_table_factory},\n {\"map\", create_stl_map_table_factory},\n {\"stl_unordered_map\", create_stl_map_table_factory},\n {\"unordered_map\", create_stl_map_table_factory},\n {\"stl_umap\", create_stl_map_table_factory},\n {\"umap\", create_stl_map_table_factory}\n};\n\n底层使用 std::unordered_map<KeyType, char*> + 手动管理的内存槽(slot buffer),带分片(shared_mutex)、inline meta(LRU/LFU 标记)、超额淘汰逻辑。\n\n七、编译时特征控制在 CMakeLists.txt 中定义:\nset(NVE_AVAILABLE_FEATURES \"\")\nlist(APPEND NVE_AVAILABLE_FEATURES\n \"abseil_plugin\" \"nvhm_plugin\" \"phmap_plugin\"\n \"redis_plugin\" \"rocksdb_plugin\"\n)\n\n\n默认(NVE_DEFAULT_FEATURES):启用全部 5 个插件 + ht_key_32 + ht_kernel_128\n最小(NVE_MINIMAL_FEATURES):仅 ht_mask_64 + ht_key_64 + ht_part_fnv1a + ht_kernel_64,无插件\n可通过 -DNVE_DISABLE_PLUGINS=1 或 -DNVE_FEATURES="nvhm_plugin redis_plugin" 定制\n\n每个启用的 feature 转换为 C++ 编译宏 -DNVE_FEATURE_{NAME}=1,用于模板分发的条件编译。\n\n八、JSON 运行时配置示例{\n \"plugins\": [\"libnve-plugin-redis.so\"],\n \"table_factories\": {\n \"my_redis_factory\": {\n \"implementation\": \"redis_cluster\",\n \"address\": \"my-redis-cluster:6379\",\n \"password\": \"****\",\n \"connections_per_node\": 10\n },\n \"my_nvhm_factory\": {\n \"implementation\": \"nvhm_map\",\n \"key_size\": 8,\n \"mask_size\": 64,\n \"max_value_size\": 8192,\n \"value_dtype\": \"float32\",\n \"num_partitions\": 4,\n \"overflow_policy\": {\n \"handler\": \"EvictLRU\",\n \"overflow_margin\": 512,\n \"resolution_margin\": 0.2\n }\n }\n },\n \"tables\": {\n \"0\": \"my_redis_factory\",\n \"1\": \"my_nvhm_factory\"\n }\n}\n\n代码调用:\nauto db = nve::build_host_database(json);\nauto& table0 = db.at(0); // Redis 集群表\nauto& table1 = db.at(1); // NVHM GPU 哈希表\n\n\n九、总结\n\n\n维度\n说明\n\n\n\n架构风格\nC 符号约定 + dlopen/dlsym 动态加载 + 抽象工厂模式\n\n\n不是\nC++ 虚基类插件,而是 extern “C” 符号 + dlsym 解析\n\n\n5 种存储后端\nGPU 端(NVHM)、CPU 端内存(Abseil/PHMAP)、远程(Redis)、持久化磁盘(RocksDB)\n\n\n内置降级\nstd::unordered_map 实现无需任何插件即可运行\n\n\n模板分发\n多维 switch 分发到编译时特化模板类,最多 6144 种组合\n\n\n构建/运行时解耦\n编译时通过 NVE_FEATURES 选择构建内容,运行时通过 dlopen 按需加载\n\n\n","categories":["inference"],"tags":["NVIDIA","Embedding","NVEmbeddingCache","Plugin","C++","dlopen"]},{"title":"tables-overview","url":"/posts/c474f32c.html","content":"Table 实现全览概述Table 是 NV Embedding Cache SDK 中所有存储后端的统一抽象基类,定义了以下纯虚接口:\nclass Table {\n virtual ~Table() = default;\n virtual void clear(context_ptr_t& ctx) = 0;\n virtual void erase(context_ptr_t& ctx, int64_t n, const void* keys) = 0;\n virtual void find(context_ptr_t& ctx, int64_t n, const void* keys,\n max_bitmask_repr_t* hit_mask, int64_t value_stride,\n void* values, int64_t* value_sizes) const = 0;\n virtual void insert(context_ptr_t& ctx, int64_t n, const void* keys,\n int64_t value_stride, int64_t value_size, const void* values) = 0;\n virtual void update(...) = 0;\n virtual void update_accumulate(...) = 0;\n virtual context_ptr_t create_execution_context(...) = 0;\n virtual void reset_lookup_counter(...) = 0;\n virtual void get_lookup_counter(...) = 0;\n virtual bool lookup_counter_hits() = 0;\n virtual int32_t get_device_id() const = 0;\n virtual int64_t get_max_row_size() const = 0;\n virtual int64_t get_invalid_key() const = 0;\n};\n\n项目实现了 8 种表,按存储位置和底层技术可分为:\n\n\n\n类别\n表实现\n存储位置\n数据持久化\n依赖\n\n\n\nGPU 缓存表\nGpuTable\nGPU 显存\n❌\ncuEmbed + EmbedCacheSA\n\n\nCPU 线性表\nLinearHostTable\nCPU 内存(用户分配)\n❌\n无(memcpy 直接读写)\n\n\nCPU 哈希表(内置)\nSTLContainerTable\nCPU 内存\n❌\nstd::unordered_map\n\n\nCPU 哈希表(Abseil)\nAbseilFlatMapTable\nCPU 内存\n❌\nAbseil Swiss Table\n\n\nCPU 哈希表(PHMAP)\nPHMapFlatMapTable\nCPU 内存\n❌\nParallel Hashmap\n\n\nCPU GPU 哈希表\nNvhmMapTable\nCPU 内存(nvhashmap)\n❌\nNVIDIA nvhashmap\n\n\n远程缓存\nRedisClusterTable\n远程 Redis 集群\n✅ 持久化\nhiredis + redis++\n\n\n磁盘存储\nRocksDBTable\n本地磁盘\n✅ 持久化\nFacebook RocksDB\n\n\n\n一、Table 基类接口详解核心操作契约find(ctx, n, keys, hit_mask, value_stride, values, value_sizes)\n\n输入:hit_mask 中 bit=0 的键需要被查找,bit=1 的键应跳过\n输出:对于 bit=0 且在表中找到的键,将其 bit 设为 1,并将值写入 values 的对应位置\n紧凑写入:如果 values 是 nullptr,则只做计数/键存在性检查\n\ninsert(ctx, n, keys, value_stride, value_size, values)\n\n向表中插入键值对。如果键已存在,行为取决于实现(替换或忽略)\n最佳努力操作:如果触发了超额淘汰,新插入的键可能立即被淘汰\n\nupdate(ctx, n, keys, value_stride, value_size, values)\n\n只覆盖表中已存在的键,不存在的键被忽略\nGpuTable 如果配置了 uvm_table 且未禁用 UVM 更新,会同时写入 UVM 后备表\n\nupdate_accumulate(ctx, n, keys, update_stride, update_size, updates, update_dtype)\n\n梯度累积模式:table[key] += updates[i]\nupdate_dtype 可以不同于表的存储类型(如 fp32 梯度累积到 fp16 表)\n\n命中率计数\n\n\n方法\n作用\n\n\n\nreset_lookup_counter(ctx)\n重置计数器\n\n\nget_lookup_counter(ctx, counter)\n读取计数器值(GPU 表需先 cudaStreamSynchronize)\n\n\nlookup_counter_hits()\nfalse = 计数的是 miss,true = 计数的是 hit\n\n\n\n二、GpuTable — GPU 集合关联缓存表头文件:include/gpu_table.hpp | 实现:src/gpu_table.cu\nGpuTable 是 NVE 的核心 GPU 端表实现,内部封装了 EmbedCacheSA<KeyType, KeyType> ——一个集合关联(Set-Associative)软件管理缓存。\n架构层次GpuTable\n └── EmbedCacheSA<KeyType, KeyType> (GPU 集合关联缓存)\n ├── EmbedCacheSA 基类: 标签匹配 + 数据区 + 替换策略\n ├── CacheSADeviceModify: 修改操作在 GPU 上执行\n └── CacheSAHostModify: 修改操作在 CPU 上执行(调试用途)\n\n查找的四种 kernel 模式当 uvm_table 非空时,run_find_uvm() 根据键数量选择执行策略:\n\n\n\nKernel 模式\n阈值条件\n行为\n\n\n\nLookupUVM\n默认:num_keys < 1M\n一次 cache->lookup 调用,cache miss 时直接从 UVM table 读取\n\n\nSortGather\n默认:num_keys ≥ 1M\n先排序键(去重),大块连续读取 UVM,改善 page fault 局部性\n\n\nPipelineGather\n用户明确指定\n流水线 gather kernel,分 tile 处理\n\n\nDynamicKernel\n—\n暂未实现\n\n\nInsert 的直方图优化GpuTable 的 insert 操作使用 DefaultGPUHistogram 在 GPU 上对键做频率统计和去重:\n输入: [1, 2, 1, 3, 2, 1, 1] (7 个键)\n ↓\nDefaultGPUHistogram(GPU kernel)\n ↓\n输出: 键 [1, 2, 3] 按频率排序 (4, 2, 1)\n ↓\ncache->insert() → 高频键优先插入 cache\n\n频率越高的键在 insert 中获得越高优先级,在集合关联缓存中更不容易被淘汰。\n\n三、LinearHostTable — CPU 线性表头文件:include/linear_host_table.hpp | 实现:src/linear_host_table.cpp\n设计理念LinearHostTable 是最简单的 CPU 端表实现:嵌入向量存储在用户提供的连续内存缓冲区中,通过 key * row_size 直接计算地址后 memcpy 读写。无哈希、无索引、无淘汰——这与 GPUEmbeddingLayer 在 GPU 端的角色完全对称。\n实现// 查找: 最简单的 memcpy\nvoid find(...) {\n thread_pool->execute_n(0, num_tasks, [=](int64_t idx) {\n for each key 跳过 hitmask 已标记的:\n memcpy(output + k * output_stride,\n src + keys[k] * row_size_in_bytes,\n row_size_in_bytes);\n });\n memset(hit_mask, 0xff, ...);\n}\n\n适用场景\n与 GpuTable 配合,UVM 后备表的 CPU 端视图\n纯 CPU 推理场景,嵌入表可完全装入系统内存\n作为 HierarchicalEmbeddingLayer 的中间层\n\n\n四、STLContainerTable — 内置 CPU 哈希表头文件:include/stl_map_backed_table.hpp | 实现:src/stl_map_backed_table.cpp\n架构STLContainerTable 是所有基于 STL 风格容器的 CPU 哈希表实现的公共基类。它使用 std::unordered_map<KeyType, char*> 作为底层映射,核心价值在于它实现了一套完整的内存管理、并发控制、超额淘汰基础设施。\nSTLContainerTable (模板基类)\n ├── 持有 std::vector<Partition>\n │ └── Partition:\n │ ├── std::shared_mutex (读写锁)\n │ ├── std::unordered_map<KeyType, char*> (键 → 槽指针)\n │ └── SlotBuffer (手动管理的内存池)\n │\n ├── 查找: 分片并发 + hitmask 过滤 + 软件预取\n ├── 插入: 分片加锁 + 槽分配 + 超额淘汰\n ├── 更新: 分片加锁 + 直接覆盖\n ├── 累积: 分片加锁 + 原子累加 + inline meta\n └── 淘汰: 三种策略 → EvictRandom / EvictLRU / EvictLFU\n\n分片与并发所有操作按 partitioner(key) & (num_partitions - 1) 分配到对应分片:\n\n查找:每个分片加共享锁(shared_lock),多线程并行\n修改:每个分片加独占锁(unique_lock),保证写一致性\n\n内存槽管理STLContainerTable 不把值直接存在 std::unordered_map 的 value 中,而是手动管理一个连续的 slot buffer:\nSlot Buffer 布局:\n┌────────────────────────────────────────────────┐\n│ Row 0 │ value_bytes │ meta_bytes │ padding│\n│ Row 1 │ value_bytes │ meta_bytes │ padding│\n│ ...... │ .............│ .............│ .......│\n│ Row N │ value_bytes │ meta_bytes │ padding│\n└────────────────────────────────────────────────┘\n\n\nvalue_bytes:嵌入向量数据\nmeta_bytes:inline metadata(LRU 时间戳或 LFU 频率计数)\nstd::unordered_map 只存 key → char*(指向 slot 起始位置)\n\n这种设计将哈希表索引和数据存储分离,元数据跟随数据一起移动,淘汰时只需排序 meta 即可决定谁被淘汰。\n超额淘汰(Overflow)当插入导致分片容量超过阈值时触发淘汰:\noverflow_margin = 512(额外条目数)\nresolution_margin = 0.2(淘汰比例 20%)\n\n触发条件: partition.size() > initial_capacity + overflow_margin\n淘汰过程:\n 1. 收集所有 key + meta\n 2. 按 meta 排序:\n - EvictRandom: 随机打乱\n - EvictLRU: 按时间戳排序(最久未使用优先)\n - EvictLFU: 按频率排序(最少使用优先)\n 3. 淘汰 resolution_margin 比例的条目\n 4. 释放对应的 slot 内存\n\n模板分发produce() 方法按 mask_size → key_size → overflow_handler → partitioner 四维 switch 分发,生成约 192 种编译时类型特化。\n内置别名系统中预注册了 6 个别名指向同一个基于 std::unordered_map 的 STLMapTableFactory:\n{\"stl_map\", \"map\", \"stl_unordered_map\", \"unordered_map\", \"stl_umap\", \"umap\"}\n\n\n五、AbseilFlatMapTable — Abseil Swiss Table 插件头文件:plugins/abseil/include/abseil_flat_map_table.hpp | 注册名:abseil_flat_map\n继承 STLContainerTable,将底层容器替换为 absl::flat_hash_map<KeyType, char*>。absl::flat_hash_map 使用 Google 的 Swiss Table 算法(开放寻址 + 元数据字节 + SIMD 查找),比 std::unordered_map 有更好的内存局部性和查找性能。\n\n\n\n维度\nSTLContainerTable\nAbseilFlatMapTable\n\n\n\n底层容器\nstd::unordered_map(链式哈希)\nabsl::flat_hash_map(Swiss Table)\n\n\n额外配置\n无\ninitial_capacity 用于 reserve()\n\n\n链接依赖\n无\nabsl_raw_logging_internal + absl_hash\n\n\n\n六、PHMapFlatMapTable — Parallel Hashmap 插件头文件:plugins/phmap/include/phmap_flat_map_table.hpp | 注册名:phmap_flat_map\n实现方式与 Abseil 插件完全相同——继承 STLContainerTable,底层替换为 phmap::flat_hash_map<KeyType, char*>。PHMAP 使用与 Abseil 类似的开放寻址算法,但实现更轻量,无外部链接依赖(header-only)。\n\n七、NvhmMapTable — NVIDIA nvhashmap 插件头文件:plugins/nvhm/include/nvhm_map_table.hpp | 注册名:nvhm_map\n与 STLContainerTable 的区别NvhmMapTable 不继承 STLContainerTable,而是直接继承 HostTable<NvhmMapTableConfig>,直接使用 NVIDIA nvhashmap 的 map 接口:\ntemplate <typename MaskType, typename MapType, typename PartitionerType>\nclass NvhmMapTable final : public HostTable<NvhmMapTableConfig> {\n struct Partition {\n mutable std::shared_mutex read_write;\n map_type map; // nvhm::map<KeyType, ValueType, ...>\n };\n std::vector<Partition> parts_;\n};\n\n配置\n\n\n字段\n默认\n说明\n\n\n\nkernel_size\nnvhm 默认\n内核大小 11024(对应 81024B entry)\n\n\nkey_fetch_queue_length\n8\n软件预取管道深度 {0,1,2,4,8}\n\n\nprefetch_values\ntrue\n是否发出值预取指令\n\n\nminimize_psl\nfalse\n缩短 probe search length\n\n\nauto_shrink\nfalse\n大量淘汰后自动缩容\n\n\n查找中的软件预取nvhashmap 的 ring_prefetch_queue 实现了软件流水线预取:\nfor (int i = 0; i < KeyFetchQueueLength; i++) {\n __builtin_prefetch(next_key_address);\n}\n\n在 find_() 模板方法中,通过 KeyFetchQueueLength 和 PrefetchValues 两个编译时参数控制预取行为,switch 生成 5 × 2 = 10 种变体。\n模板分发NvhmMapTable 的模板分发维度最多:\nmask_size × key_size × overflow_handler × kernel_size × minimize_psl × auto_shrink × partitioner\n→ 约 6144 种编译时类型组合。\n\n八、RedisClusterTable — Redis 远程表头文件:plugins/redis/include/redis_cluster_table.hpp | 注册名:redis_cluster\n架构RedisClusterTable 是 NVE 中唯一基于网络通信的表实现。它通过 hiredis(C 客户端)和 redis++(C++ 客户端)连接到外部 Redis Cluster,将嵌入向量以序列化形式存储在 Redis 中。\n配置\n\n\n字段\n默认\n说明\n\n\n\naddress\nlocalhost:6379\nRedis 集群任意节点地址\n\n\nuser_name\ndefault\n用户名\n\n\npassword\n空\n密码\n\n\nconnections_per_node\n5\n每节点最大并行连接数\n\n\nconnection_lifetime\n180s\n连接生命周期\n\n\nmax_batch_size\n16384\n批量操作最大大小\n\n\nnum_partitions\n1\n并行分片数(2 的幂)\n\n\n查找实现auto reply = redis_cluster_->pipeline(keys.begin(), keys.end(),\n [](const auto& key) { return sw::redis::command(\"GET\", key); });\n\n对于每个分片,使用 redis++ 的 pipeline 功能批量发送 GET 命令,减少网络往返次数。\n链接依赖hiredis + hiredis_ssl + redis++_static + ssl + crypto\n\n九、RocksDBTable — 磁盘持久化表头文件:plugins/rocksdb/include/rocksdb_table.hpp | 注册名:rocksdb\n架构RocksDBTable 通过 Facebook RocksDB 提供本地磁盘持久化存储。嵌入向量以 key → value 形式存储在 RocksDB 的 Column Family 中。\n配置\n\n\n字段\n默认\n说明\n\n\n\ncolumn_family\ndefault\nRocksDB Column Family\n\n\nmax_batch_size\n16384\n批量操作最大大小\n\n\nverify_checksums\ntrue\n是否校验 checksum\n\n\npath\n/tmp/rocksdb\n数据库文件路径\n\n\nread_only\nfalse\n只读模式(允许多进程并发读取)\n\n\nnum_threads\n16\n后台线程数\n\n\n查找实现db_->MultiGet(read_opts_, col_families_, keys_slices, &values, &statuses);\n\n使用 RocksDB 的 MultiGet 实现批量读取,单次调用返回多个键的状态。\n\n十、所有 Table 的对比总表\n\n\n维度\nGpuTable\nLinearHostTable\nSTLContainer\nAbseilFlatMap\nPHMapFlatMap\nNvhmMapTable\nRedisCluster\nRocksDB\n\n\n\n存储位置\nGPU 显存\nCPU 线性内存\nCPU 堆内存\nCPU 堆内存\nCPU 堆内存\nCPU 堆内存\n远程集群\n本地磁盘\n\n\n数据持久化\n❌\n❌\n❌\n❌\n❌\n❌\n✅\n✅\n\n\n查找算法\n集合关联 tag\nkey * stride\nunordered_map O(1)\nSwiss Table O(1)\nPHMAP O(1)\nnvhashmap O(1)\nRedis GET\nLSM-Tree\n\n\n并发粒控\nCUDA stream\n线程池并行\n分片读写锁\n分片读写锁\n分片读写锁\n分片读写锁\npipeline + 连接池\nRocksDB 内部\n\n\n淘汰策略\n集合关联 LRU\n无(固定数组)\nEvictRandom/LRU/LFU\n同左\n同左\n同左\n无(Redis 自管)\n无(磁盘存储)\n\n\n是否插件\n核心库内置\n核心库内置\n核心库内置\n✅ abseil 插件\n✅ phmap 插件\n✅ nvhm 插件\n✅ redis 插件\n✅ rocksdb 插件\n\n\n启动依赖\nCUDA + nvhashmap\n无\n无\nabsl\nPHMAP header-only\nnvhashmap\nhiredis + redis++\nlibrocksdb\n\n\nJSON 实现名\n—\n—\nstl_map/umap\nabseil_flat_map\nphmap_flat_map\nnvhm_map\nredis_cluster\nrocksdb\n\n\n典型扮演角色\n层级首表\n层级中/末表\n层级中层\n层级中层\n层级中层\n层级中层\n层级末表\n层级末表\n\n\n\n十一、在嵌入层中的角色总结HierarchicalEmbeddingLayer:\n tables[0] ─── GpuTable (GPU cache, 最快层)\n tables[1] ─── NvhmMapTable / AbseilFlatMapTable / STLContainerTable (CPU hash, 中间层)\n tables[2] ─── RedisClusterTable / RocksDBTable (远程/持久化, 末层)\n\nLinearUVMEmbeddingLayer:\n GpuTable.cache_ = EmbedCacheSA (GPU cache)\n GpuTable.config_.uvm_table = UVM 线性内存 (硬件透明回退)\n\nGPUEmbeddingLayer:\n 无 Table (直接 cuEmbed Forward kernel, 线性 GPU 内存)","categories":["inference"],"tags":["NVIDIA","Embedding","GPU","NVEmbeddingCache","Table","CPU","Redis","RocksDB"]},{"title":"tensorflow源码阅读--前置准备","url":"/posts/f4cfcd39.html","content":"简介准备写几个文章来记录对tensorflow代码的阅读。本文主要写tensorflow代码阅读准备。\n准备工作代码阅读前,还是需要准备一下装备来提升代码阅读效率,好的工具能提升代码阅读的效率,这里推荐使用CLion来阅读代码。即使windows下,也可以用CLion阅读代码,虽然很多代码编译不过,但并不影响代码的阅读。\n首先,我们需要选择一个阅读的版本,本文选择1.15版本。在github上建立代码库tf_read,然后在该目录下下载tensorflow/tensorflow和tensorflow/serving的代码(直接下载对应的tar包解压就行)。目前tf_read目录下的情况如下:\n\nserving-1.15.0 tf-serving的源码\ntensorflow-1.15.0 tf的源码\n\n为了提升使用CLion阅读代码的时候效率,需要提前做一些工作,包括:生成pb产物、移除单测文件、屏蔽“无关”文件、添加构建目标。\n生成pb产物CLion代码分析时,会查找对应的头文件,如果proto编译产物不存在,则会影响代码的解析,也会影响代码跳转的准确率。这时,我们需要手工编译一下proto文件,但是需要将编译产物放在特定的目录下,并将这个目录从全文搜索的路径里移除,避免搜索proto成员时出现一堆编译产物的搜索结果。在根目录下创建pb_out目录用于存放proto编译产出。在serving-1.15.0目录下建立一个指向tensorflow-1.15.0/tensorflow的软链,因为serving下的部分proto依赖了tensorflow的proto。分别在serving-1.15.0和tensorflow-1.15.0目录下执行下面代码,进行手工编译:\nprotoc --cpp_out=../pb_out `find . -name '*.proto'`\n\n然后在serving-1.15.0目录下单独执行grpc的编译命令:\nprotoc --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` `find tensorflow_serving/apis/ -name '*.proto'`\n\n上面命令执行完毕后,选中pb_out目录,右键-将目录标记为-排除。接着在CMakeLists.txt中添加include_directories(pb_out)。\n移除单侧代码由于tensorflow和serving是使用bazel构建的,单侧文件和源码文件都是放在一个目录里,影响CLion的全文搜索和“查找用法”等功能。这时建议把”_test.cc”和”_benchmark.cc”文件移动到特定目录下。在根目录下建立test_dir目录,然后编写脚本将这两类文件移动到这个目录下:\nfor filename in `find . -name '*_test.cc' -o -name '*_benchmark.cc'`\ndo\n echo ${filename};\n origin_dir=`dirname ${filename}`;\n target_dir="test_dir/${origin_dir}";\n echo ${target_dir};\n mkdir -p ${target_dir};\n cp ${filename} ${target_dir};\ndone\n\n执行上面的脚本后,单侧和压测代码就被移动到test_dir目录下,然后右键-将目录标记为-排除,这样就排除单侧对代码跳转的影响了。\n屏蔽“无关”文件为了减少全文搜索时无关代码对结果的影响,建议将py、java、go、lite等相关代码目录直接排除(右键-将目录标记为-排除)。\n添加构建目标tensorflow是使用bazel构建的,虽然CLion有bazel插件,但是bazel经常崩溃,体验不是特别好,所以还是使用cmake来构建这个代码。在CMakeLists.txt里添加下面代码,来添加构建目标(不是真的构建,只是用来阅读一下代码):\nset(CMAKE_CXX_STANDARD 17)\n\ninclude_directories(tensorflow-1.15.0)\ninclude_directories(serving-1.15.0)\n\naux_source_directory(serving-1.15.0/tensorflow_serving/apis SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/batching SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/config SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/core SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/model_servers SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/resources SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/servables/hashmap SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/servables/tensorflow SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/sources/storage_path SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/util SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/util/net_http/client SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/util/net_http/compression SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/util/net_http/internal SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/util/net_http/server/internal SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/util/net_http/server/public SERVER_SRC)\naux_source_directory(serving-1.15.0/tensorflow_serving/mytools SERVER_SRC)\n\nadd_executable(tf-server ${SERVER_SRC})\n\naux_source_directory(tensorflow-1.15.0/tensorflow/cc/client TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/cc/framework TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/cc/gradients TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/cc/ops TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/cc/profiler TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/cc/saved_model TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/cc/tools TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/cc/training TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/api_def TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/common_runtime TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/common_runtime/data TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/common_runtime/eager TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/common_runtime/gpu TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/common_runtime/sycl TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/debug TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/distributed_runtime TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/distributed_runtime/eager TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/distributed_runtime/rpc TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/framework TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/graph TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/grappler TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/grappler/clusters TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/grappler/costs TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/grappler/graph_analyzer TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/grappler/inputs TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/grappler/optimizers TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/grappler/utils TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/grappler/verifiers TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/kernels TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/kernels/batching_util TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/kernels/boosted_trees TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/kernels/boosted_trees/quantiles TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/kernels/data TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/kernels/fuzzing TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/kernels/hexagon TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/kernels/neon TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/kernels/rnn TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/kernels/tensor_forest TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/lib TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/lib/core TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/nccl TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/ops TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/ops/compat TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/platform TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/platform/cloud TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/platform/default TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/platform/hadoop TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/platform/posix TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/platform/profile_utils TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/platform/s3 TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/profiler TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/profiler/internal TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/profiler/lib TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/profiler/rpc TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/protobuf TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/summary TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/tpu TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/user_ops TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/util TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/util/ctc TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/util/proto TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/util/rpc TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/util/sparse TF_SRC)\naux_source_directory(tensorflow-1.15.0/tensorflow/core/util/tensor_bundle TF_SRC)\n\nadd_library(tf ${TF_SRC})\n\n部分第三方库不在源码中,这时需要手工安装一下。mac用户可以直接用brew进行安装,同时在CMakeLists.txt里添加include_directories(/usr/local/include); windows用户直接使用conan安装一下即可,conanfile.txt内容如下:\n[build_requires]\n\n[requires]\nprotobuf/3.9.1\ngflags/2.2.2\nglog/0.5.0\nabseil/20211102.0\neigen/3.4.0\n\n[generators]\ncmake_find_package\ncmake_paths\n\n[options]\n然后在CMakeLists.txt中添加下面内容:\ninclude(${CMAKE_BINARY_DIR}/conan_paths.cmake)\nfind_package(absl REQUIRED)\nfind_package(Protobuf REQUIRED)\nfind_package(Eigen3 REQUIRED)\ninclude_directories(${absl_INCLUDE_DIR})\ninclude_directories(${protobuf_INCLUDE_DIR})\nget_filename_component(Eigen3_INCLUDE_DIR_P ${Eigen3_INCLUDE_DIR} DIRECTORY)\ninclude_directories(${Eigen3_INCLUDE_DIR_P})\n\n使用CLion看tensorflow的准备工作就做完了。\n","categories":["inference"],"tags":["c++","tensorflow"]},{"title":"tensorflow-serving--模型管理","url":"/posts/ef7468b9.html","content":"简介准备写几个文章来记录对tensorflow代码的阅读。本文自顶向下介绍tensorflow-serving的架构,着重介绍一下模型管理及加载机制。\n整体架构tensorflow-serving(下文简称tfserving)是对tensorflow的封装,对外提供服务接口,关系类似lucene与es。tfserving整体可以分为三层,接口层、模型管理层、tf引擎层,如下图所示:\n\ntfserving使用grpc作为服务框架,提供在线推理和模型状态查询的接口。与此同时,基于libevent实现了一个http服务,除了在线推理和模型状态查询接口外,还提供了prometheus接口用于监控。\n模型管理层是serving的核心部分,负责维护模型的生命周期,重要的组件包括AspiredVersionsManager、BasicMananger、Adapter、Router、StoragePathSource等,下文将着重介绍。\ntf引擎层实际是对tensorflow的封装,tfserving将其封装为Servable,通过模型名及其版本确认对应的模型,即三元组(name, version, model)。\n资源交互这部分讲一下从请求到来到推理进行里过程。以推理为例子,调用层次如下:\n\nPredictionServiceImpl::Predict调用TensorflowPredictor::Predict进行预估\nTensorflowPredictor::Predict调用TensorflowPredictor::PredictWithModelSpec进行预估\n调用ServerCore::GetServableHandle获取SavedModelBundle\n调用ServerCore::ServableRequestFromModelSpec构建查询包\n调用AspiredVersionsManager::GetServableHandle获得servable\nAspiredVersionsManager::GetUntypedServableHandle 调用\nBasicManager::GetUntypedServableHandle 调用\nServingMap::GetUntypedServableHandle\n\n\n根据ServableRequest查询HandlesMap获取对应的LoaderHarness\nLoaderHarness取出Loader指针,使用SharedPtrHandle包裹后返回。\n\n\n\n\n\n\n调用internal::RunPredict进行预估\n输入映射(PreProcessPrediction)\n执行Session::Run进行预估(已经在tensorflow代码里)\n输出打包(PostProcessPredictionResult)\n\n\n\n\n\nTensorflowPredictor类是对推理流程的封装,保障grpc和http接口都能进行推理。ServerCore类承接服务和模型。\nServerCore下的组件:\n\nAspiredVersionsManager 负责装载策略,控制装载卸载的模型版本\nBasicMananger 资源管理,提供资源管理、资源托管、资源装载卸载功能\n\n\nServableStateMonitor 状态监视器,处理状态变化,并触发回调。\nEventBus 消息总线,连接ServableStateMonitor和AspiredVersionsManager。\nStoragePathSourceAndRouter\nResourceTracker\n\n模型管理tfserving模型管理功能包括三个部分,模型配置热加载、模型版本侦测、模型加载卸载。\ntfserving支持单模型和多模型两种模式,单模型通过入参控制加载的模型;多模型通过配置文件控制模型名、模型路径等。模型配置热加载机制是针对多模型模式。\ntfserving在这部分抽象了很多层,其目标是支持多种文件系统、支持多个平台(tf或者其他)、支持多个模型、支持模型的多个版本。考虑到这点就不难理解这个多层设计了。 \n模型配置热加载模型配置热加载机制是创建一个定时线程,按照入参配置的时间间隔,读取指定的配置文件,然后调用ServerCore::ReloadConfig函数,重新加载模型配置。这个地方可能出现内存泄漏问题。\n创建定时线程的代码在tensorflow_serving/model_servers/server.cc:343;配置读取逻辑在tensorflow_serving/model_servers/server.cc:174;配置应用的逻辑在tensorflow_serving/model_servers/server_core.cc:431。\n配置应用逻辑里,会先校验配置内容,然后更新模型对应的配置信息(模型的路径)。\n模型版本侦测版本侦测就涉及到一套回调机制了,Source::AspiredVersionsCallback,在代码serving-1.15.0/tensorflow_serving/core/source.h:87。上层对象将回调函数注册给下层对象,下层对象侦测到状态后,调用这个回调函数,然后将信息传递给上层对象。\n这里涉及到StoragePathSource、Router、Adapter、Manager等多个实例,他们之间的调用如下:\nStoragePathSource --> Router --> Adapter --> AspiredVersionManager\n\n这里就牵扯到Source和Target关系了,即源和目标。Target提供回调函数给Source,Source按需调用该函数,回调绑定通过ConnectSourceToTarget连接。Router和Adapter既是Source又是Target。ConnectSourcesWithFastInitialLoad中绑定了AspiredVersionManager和Adapter。一个平台对应一个StoragePathSourceAdapter,存在SourceAdapters中。\nStoragePathSource是TFS定义的对未加载模型对象的抽象, 目前实现了两种Source, 一种是StaticStoragePathSource,一种是FileSystemStoragePathSource. 前者是简单的静态的模型文件存储系统, 仅仅在启动时触发模型的加载, 没有其他动作. 后者是动态的Source, 能监测存储系统的变化并发出通知.\ntfserving实现Source时将模块职责划分的很清晰, Source的职责就是监测变化, 如何处理则由Source的用户决定, 所以Source有一个接口SetAspiredVersionsCallback, 可以设置回调函数用于通知AspiredVersion的变化. Source在变化的时候就会调用设置的回调函数.\n作为Source的对等对象, 系统也定义了Target, 有接口GetAspiredVersionsCallback, 用于获取处理AspiredVersions的回调接口, 然后我们就可以将Target和Source连起来了.\n上述连接关系里面, Router和Adapter既是Source又是Target, AspiredVersionManager是Target. 但是Router没有实现Source接口, 而是要求在创建Router对象时直接将Adapter作为参数, 这样实现主要目的是创建一对多的关系.\n系统根据所支持平台的个数(tensorflow算是一种平台)创建Adapter, 一种平台对应一个Adapter, 负责创建模型加载器Loader. 对于tensorflow平台, 对应的adapter是SavedModelBundleSourceAdapter.\nRouter负责根据模型名称查找对应的平台(model_config里面有指定平台名称), 从而定位到对应的Adapter.\n这些连接关系是在系统启动, 或者更新model-config的时候建立的.\n默认配置下, FileSystemStoragePathSource为Source的实例, SavedModelBundleSourceAdapter为Adapter的实例, DynamicSourceRouter为Router的实例.\nFileSystemStoragePathSource有自己单独的工作线程, 周期查询文件系统, 发现每个模型的版本, 根据指定的servable_version_policy(model_config), 创建ServableData(模型名, 版本号, 路径), 传给RouterRouter根据路由找到对应的adapter, 传给AdataperAdapter将ServableData(模型名, 版本号, 路径)转换成ServableData(模型名, 版本, Loader), 传给AspiredVersionManagerAspiredVersionManager将这些信息存到pending_aspired_versions_requests_, 等待另外一个工作线程(AspiredVersionsManager_ManageState_Thread)处理\n如下图左侧所示: \n\n由于tfserving实际就 tensorflow这个平台,所以,上图实际情况如下图所示:\n\n模型加载卸载模型的装在卸载就需要详细介绍一下AspiredVersionsManager、BasicMananger、servable类了。AspiredVersionsManager是总控,一端连着版本发现,一端控制模型加载,内部有个更新线程进行将待更新队列中的数据取出,进行版本更新。BasicMananger存粹是维护模型的版本数据;servable是存粹的模型版本。\nAspiredVersionsManager类AspiredVersionsManager负责装载策略,控制装载卸载的模型版本。成员对象包括:\n\naspired_version_policy_ 装载策略\npending_aspired_versions_requests_ 待处理资源\nmanage_state_thread_ 定时任务,负责处理状态\ntarget_impl_ 应该是IMPL模式对象\nbasic_manager_ BasicMananger类对象,资源管理\nset_num_load_threads_observer_\n\n这个类对外(ServerCore)只提供了查询Servable的接口,用于获取模型。这个类的对象创建之后,会启动一个后台线程去周期执行任务:\n\nFlushServables 清理(取消托管)无用或者状态有问题的资源\nHandlePendingAspiredVersionsRequests\n当前资源正在卸载中的,跳过本次处理\n之前出现镜像版本不在新增版本中 (应该是该版本本地数据被删除,但是还没被卸载)\n这次该版本出现在新增版本中\n\n\n处理选中版本逻辑\n获取新版本列表\n获取当前版本列表\n镜像版本不在新版本中,进行标记(本地数据已经被删除)\n获取差集(新版本 - 当前版本)进行托管\n\n\n\n\nInvokePolicyAndExecuteAction 筛选一个操作(模型装载或卸载),并执行\n获取每个模型的每个版本的镜像状态\n针对每个模型,使用装载策略器筛选一个“操作”(至多一个)\n针对所有模型的“操作”,筛选(优先卸载)一个执行 调用basic_manager的装载或者卸载接口进行操作。\n\n\n\npending_aspired_versions_requests_存储等待处理队列,调用关系有点绕:\nAspiredVersionsManagerTargetImpl::SetAspiredVersions\nAspiredVersionsManager::EnqueueAspiredVersionsRequest\n添加pending_aspired_versions_requests_\n\n装载策略:\n\nAvailabilityPreservingPolicy\nResourcePreservingPolicy\n\nBasicMananger类BasicManager,负责资源管理,提供资源获取、资源托管、资源装载、资源卸载,主要结构如下:\n\nservable_event_bus_ 状态消息分发\nmanaged_map_ 已托管的资源表,multimap<模型名, LoaderHarness_Ptr>\nserving_map_ 就绪的模型表类,类型是ServingMap,里面也是multimap结构\nload_executor_和unload_executor_是装载、卸载的线程池\n\nBasicManager提供的接口:\n\nManageServable 托管资源\n从ServableData对象中取出Loader和ServableId,装入LoaderHarness\n发布消息<ServableId, kStart>\n加入managed_map_中进行管理\n\n\nLoadServable 装载资源,资源必须被托管\n构建LoadOrUnloadRequest对象\n获取LoaderHarness对象\n检查LoaderHarness对象是否已经装载\n调度进行装载,配置了线程池就用线程池\n获取LoaderHarness对象\n预先分配资源\n修改资源状态为kLoadApproved\n发布事件kLoading\n调度LoaderHarness的Load函数进行装载\n更新ServingMap数据\n发布事件kAvailable\n\n\n\n\nUnloadServable 卸载资源\n构建LoadOrUnloadRequest对象\n获取LoaderHarness对象\n检查LoaderHarness对象是否已经卸载\n调度进行卸载,配置了线程池就用线程池\n获取LoaderHarness对象\n修改资源状态为kQuiescing\n发布事件kUnloading\n更新ServingMap数据\n修改新状态为kQuiesced\n调度LoaderHarness的Unload函数进行卸载\n发布事件kEnd\n\n\n\n\nStopManagingServable 取消托管资源\n从managed_map_中删除资源\n\n\nGetUntypedServableHandle 获取资源\n从serving_map_中获取资源\n查询unordered_multimap里查询LoaderHarness对象\nLoaderHarness中取出ServableId和Loader,装入SharedPtrHandle\n\n\n\n\n其他查询接就不介绍了\n\nservable类一个servable对应一个模型的一个版本,这里涉及到多个类:\n\nSavedModelBundle 实际模型\nSession 对象\nmeta_graph_def graph申明\n\n\nServableHandle 类似智能指针,但是不管理内存\nUntypedServableHandle 基类,定义了获取servable的句柄。\nSharedPtrHandle UntypedServableHandle的实现,从Loader中获取servable对象。\nServableRequest 用来查询manager获得指定版本模型的阐述对象(Params)\n\n他们的关系是: manager中存了SharedPtrHandle对象,通过ServableRequest查询对应的SharedPtrHandle;使用时将获取的使用ServableHandle将获取的SharedPtrHandle对象中的Loader维护的SavedModelBundle取出。\n更新流程AspiredVersionsManager对象从队列中取到版本,跟自身的版本进行比较,得到需要更新的版本数据。AspiredVersionsManager调用BasicMananger::LoadServable,装载模型。BasicMananger进行一连串调用之后,LoaderHarness、SimpleLoader等之后,就到tensorflow里的代码了。\n","categories":["inference"],"tags":["c++","tensorflow","serving","模型","推理"]},{"title":"tfserving模型策略整理","url":"/posts/ad28feb4.html","content":"简介本文总结一下tensorflow serving的模型加载策略,以及模型serving策略。\n模型加载策略tf-serving支持多平台、多模型、多版本,支持制定模型维度的加载策略。这个策略配置在serving-1.15.0/tensorflow_serving/config/model_server_config.proto中,字段如下:\n// Version policy for the model indicating which version(s) of the model to\n// load and make available for serving simultaneously.\n// The default option is to serve only the latest version of the model.\n//\n// (This can be changed once a model is in serving.)\nFileSystemStoragePathSourceConfig.ServableVersionPolicy model_version_policy = 7;\n\n这个字段的定义在serving-1.15.0/tensorflow_serving/sources/storage_path/file_system_storage_path_source.proto中,内容如下:\n// A policy that dictates which version(s) of a servable should be served.\nmessage ServableVersionPolicy {\n // Serve the latest versions (i.e. the ones with the highest version\n // numbers), among those found on disk.\n //\n // This is the default policy, with the default number of versions as 1.\n message Latest {\n // Number of latest versions to serve. (The default is 1.)\n uint32 num_versions = 1;\n }\n\n // Serve all versions found on disk.\n message All {\n }\n\n // Serve a specific version (or set of versions).\n //\n // This policy is useful for rolling back to a specific version, or for\n // canarying a specific version while still serving a separate stable\n // version.\n message Specific {\n // The version numbers to serve.\n repeated int64 versions = 1;\n }\n\n oneof policy_choice {\n Latest latest = 100;\n All all = 101;\n Specific specific = 102;\n }\n}\n\n末尾policy_choice中可以判断使用的是哪种:\n\nlatest 表示只加载最新版本\nall 加载所有版本\nspecific 指定版本(可以多个版本)\n\n除此之外,AspiredVersionsManager类有两种版本管理策略:\n\nAvailabilityPreservingPolicy 先加载新版本,后卸载老版本\nResourcePreservingPolicy 先卸载老版本,后加载新版本\n\n模型serving策略模型serving策略实际上是在加载策略基础上实现的。在模型对应的配置中有字段version_labels,定义如下:\n// String labels to associate with versions of the model, allowing inference\n// queries to refer to versions by label instead of number. Multiple labels\n// can map to the same version, but not vice-versa.\n//\n// An envisioned use-case for these labels is canarying tentative versions.\n// For example, one can assign labels \"stable\" and \"canary\" to two specific\n// versions. Perhaps initially \"stable\" is assigned to version 0 and \"canary\"\n// to version 1. Once version 1 passes canary, one can shift the \"stable\"\n// label to refer to version 1 (at that point both labels map to the same\n// version -- version 1 -- which is fine). Later once version 2 is ready to\n// canary one can move the \"canary\" label to version 2. And so on.\nmap<string, int64> version_labels = 8;\n\n这个字段可以指定版本的标签。\n接着在请求包有的model_spec字段中,可以指定版本或者指定标签,如下:\n// Metadata for an inference request such as the model name and version.\nmessage ModelSpec {\n // Required servable name.\n string name = 1;\n\n // Optional choice of which version of the model to use.\n //\n // Recommended to be left unset in the common case. Should be specified only\n // when there is a strong version consistency requirement.\n //\n // When left unspecified, the system will serve the best available version.\n // This is typically the latest version, though during version transitions,\n // notably when serving on a fleet of instances, may be either the previous or\n // new version.\n oneof version_choice {\n // Use this specific version number.\n google.protobuf.Int64Value version = 2;\n\n // Use the version associated with the given label.\n string version_label = 4;\n }\n\n // A named signature to evaluate. If unspecified, the default signature will\n // be used.\n string signature_name = 3;\n}\n\nserving服务处理请求时,根据version_choice的取值来选择版本,默认是选择最新版本,代码如下:\nswitch (model_spec.version_choice_case()) {\n case ModelSpec::kVersion: {\n *servable_request = ServableRequest::Specific(model_spec.name(), model_spec.version().value());\n break;\n }\n case ModelSpec::kVersionLabel: {\n if (!options_.allow_version_labels) {\n return errors::InvalidArgument("ModelSpec has 'version_label' set, but it is not currently allowed by the server.");\n }\n int64 version;\n TF_RETURN_IF_ERROR(GetModelVersionForLabel(model_spec.name(), model_spec.version_label(), &version));\n *servable_request = ServableRequest::Specific(model_spec.name(), version);\n break;\n }\n case ModelSpec::VERSION_CHOICE_NOT_SET: {\n *servable_request = ServableRequest::Latest(model_spec.name());\n break;\n }\n}\n\n上面的模型选择需要与模型加载机制相配置,如果只加载了一个版本的模型,这时如果指定版本或者标签,就有可能找不到对应的模型,这时就会返回标签不存在或者模型版本找不到的错误,然后请求失败,并没有兜底请求一说。\n","categories":["inference"],"tags":["tensorflow","serving","model"]},{"title":"tensorflow模型数据解析","url":"/posts/e1fa92aa.html","content":"简介本文将介绍一下tensorflow的saved model数据的结构。\n模型文件tensorflow的saved model的目录结构如下:\n\nsaved_model.pb 模型描述文件,pb二进制; 也可以是pbtxt后缀的文本文件\nvariables 模型参数数据目录\nvariables.index 模型索引文件\nvariables.data-00000-of-00001 模型数据文件\n\n\nassets.extra\ntf_serving_warmup_requests 预热请求包,pb二进制文件\n\n\n\n模型加载流程参考上一篇文章中模型加载流程,模型加载时会调用LoadSavedModel函数用于加载模型。这个函数调用LoadSavedModelInternal进行实际的加载操作:\n\nReadMetaGraphDefFromSavedModel\nLoadMetaGraphIntoSession\nGetAssetFileDefs\nRunRestore\n\n其中RunRestore函数中加载了参数数据,具体操作是执行该模型图中bundle->meta_graph_def.saver_def().restore_op_name()算子。\n以half_plus_two_pbtxt为例,文件在tensorflow-1.15.0/tensorflow/cc/saved_model/testdata/half_plus_two_pbtxt/00000123/saved_model.pbtxt。图中saver_def内容如下:\nsaver_def {\n filename_tensor_name: "save/Const:0"\n save_tensor_name: "save/Identity:0"\n restore_op_name: "save/restore_all"\n max_to_keep: 5\n sharded: true\n keep_checkpoint_every_n_hours: 10000.0\n version: V2\n}\n\n加载时需要执行”save/restore_all”算子, 由于依赖关系,实际上需要执行多个算子。使用netron工具打开图描述,可以看到如下:\n\n它本身是个NoOp,主要依赖了RestoreV2算子和Assign算子。其中,RestoreV2算子实际加载了数据。RestoreV2算子代码在tensorflow-1.15.0/tensorflow/core/kernels/save_restore_v2_ops.cc中。\n整个流程包括:\n\n查询index文件的index段, 获取data块的信息\n查询index文件的data段中的块,获得数据段信息\n根据数据段信息,从data文件中截取数据.\n\n整个调用栈:\n\nRestoreV2::Compute 数据加载\nValidateInputs 校验\nRestoreTensorsV2 加载数据\n校验tensor_name的类型\n遍历拆分队列(维度多的多线程跑,维度小的单线程跑)\nRestoreOp::run 加载一个tensor name\nBundleReader::LookupTensorShape 获取形状\nBundleReader::Lookup 没设置shape_and_slice\nBundleReader::LookupSlice 有设置 shape_and_slice\n\n\n\n\n\n\n\nRestoreOp::run运行时会创建BunleReader对象,这里涉及到一个初始化过程:\n\n解析footer\n加载index文件的index段\n读取header数据\n\nheader的结构为BundleHeaderProto, 定义如下:\nmessage BundleHeaderProto {\n // Number of data files in the bundle.\n int32 num_shards = 1;\n\n // An enum indicating the endianness of the platform that produced this\n // bundle. A bundle can only be read by a platform with matching endianness.\n // Defaults to LITTLE, as most modern platforms are little-endian.\n //\n // Affects the binary tensor data bytes only, not the metadata in protobufs.\n enum Endianness {\n LITTLE = 0;\n BIG = 1;\n }\n Endianness endianness = 2;\n\n // Versioning of the tensor bundle format.\n VersionDef version = 3;\n}\n\nheader数据的key为空字符串,header中保存了分片数量,即data文件的数量。\nBundleReader::Lookup的逻辑如下:\n\nBundleReader::GetBundleEntryProto 获取pb结构\nBundleReader::Seek\nTwoLevelIterator::Seek\nindex_iter_->Seek 在index块中找到target\nTwoLevelIterator::InitDataBlock 设置data_iter(没有就创建)\ndata_iter_->Seek 在data块中查找target\nTwoLevelIterator::SkipEmptyDataBlocksForward\n\n\nParseEntryProto 数据解析成BundleEntryProto结构\n\n\n\n\nGetValue 根据BundleEntryProto结构中的信息,在data文件中读取数据\nGetSliceValue 分段读取数据\n\n其中GetValue和GetSliceValue是两选一。如果在调用BundleReader::GetBundleEntryProto拿到的BundleEntryProto结构中有slices结构,则表示数据进行分段了,需要拼接一组新的key,然后依次调用BundleReader::GetBundleEntryProto和GetValue获取数据.\n// Describes the metadata related to a checkpointed tensor.\nmessage BundleEntryProto {\n // The tensor dtype and shape.\n DataType dtype = 1;\n TensorShapeProto shape = 2;\n // The binary content of the tensor lies in:\n // File "shard_id": bytes [offset, offset + size).\n int32 shard_id = 3;\n int64 offset = 4;\n int64 size = 5;\n\n // The CRC32C checksum of the tensor bytes.\n fixed32 crc32c = 6;\n\n // Iff present, this entry represents a partitioned tensor. The previous\n // fields are interpreted as follows:\n //\n // "dtype", "shape": describe the full tensor.\n // "shard_id", "offset", "size", "crc32c": all IGNORED.\n // These information for each slice can be looked up in their own\n // BundleEntryProto, keyed by each "slice_name".\n repeated TensorSliceProto slices = 7;\n}\n\n当slices为非空时,表示当前数据采用了分段的方式,就会调用GetSliceValue。这个函数中会根据tensor_name和slices数据,生成一个key列表,然后再走一边BundleReader::GetBundleEntryProto和GetValue获取实际的数据。\n模型数据结构如果看过leveldb源码,就会发现,这两个代码的分段逻辑出奇的相似。index文件分为index段、data段和footer段三部分。\nfooter段为48字节,末尾8个字节是magic字段用于校验,前面40字节中存储了四个vint64的数据,分别是:\n\nmeta_index_handle\noffset\nsize\n\n\nindex_handle\noffset\nsize\n\n\n\n其中index_index_handle记录index段的位置。index段包括三个部分,数据部分(index_handle.size大小)、压缩flags(1字节)、校验位(4字节)。数据部分包括entry结构数组、偏移数组、entry数量(4字节),其中entry数组和偏移数组长度相同。entry结构数组中的entry是按照entry.key进行排序的,也就是一个有序数组。根据key进行查询时,先二分在偏移数组中查找,比较阶段会读取entry中的key进行比较。找到index后,在entry中线性搜索,确认是否真是存在。\nentry中包含五个部分:\n\nshared 固定为0,\nnoshared key的长度\nvalue_length value的长度\nkey key数据\nvalue value数据\n\nshared字段存在的原因应该是跟leveldb结构保持一致,leveldb中块中的key分为前缀和后缀。shared字段恒为0,表示并没有使用这个前缀后缀机制。\ndata段的数据结构跟index段数据结构基本一致。差别是index段中的value存两个vint64字段,分别表示key所在的data段的偏移和长度。也就是查询时,先在index段中找到data段的偏移和长度,然后在众多data段中再次查找,找到对应的value。\ndata段中的value是pb二进制文件,前面已经介绍过。\nindex文件的结构如下:\n\ndata文件结构相对简单,data文件的数量记录在header中,即key为空字符串的value里。\nindex文件里查询出的BundleEntryProto结构,可以定位到(shared_id, offset, size)三元组,直接读取即可。\n","categories":["inference"],"tags":["tensorflow","saved model"]},{"title":"深入浅出triton-inference-server","url":"/posts/11fad353.html","content":"简介triton-inference-server是英伟达开源的推理服务,支持多种推理引擎、支持多模型多版本,同时支持自定义后端,方便用户拓展。\n环境搭建及使用镜像构建triton是使用cmake构建的服务,本身有多个代码库组成:\n\ncore 服务核心代码\ncommon 公共使用的工具(proto)\nbackend 推理引擎接口\nserver 服务代码\nthird_party 一些魔改后的三方库\nxxx_backend 使用backend接口封装的各个推理引擎\n\nserver这个二进制会静态链接core、common、backend的代码,形如xxx_backend都是以动态链接库的形式。\nserver提供了build.py脚本,用于构建,主要的内容包括,根据入参生成编译脚本和对应dockerfile,然后启动镜像进行编译。这里的参数包括,需要的引擎、需要开启的功能等。\n执行./build.py -v --enable-all --dryrun,只生成脚本,并不实际执行,生成的文件包括:\n\nBuild/cmake_build 基于cmake构建组件的脚本\nBuild/docker_build 构建docker镜像的脚本\nBuild/Dockerfile 运行镜像\nBuild/Dockerfile.buildbase 构建镜像\nBuild/Dckerfile.cibase 流水线镜像\n\n例如cpu版本的构建命令如下:\n./build.py -v \\\n--enable-logging \\\n--enable-stats \\\n--enable-metrics \\\n--enable-tracing \\\n--backend=identity \\\n--backend=repeat \\\n--backend=ensemble \\\n--backend=square \\\n--backend=tensorflow2 \\\n--backend=onnxruntime \\\n--backend=openvino \\\n--backend=pytorch \\\n--backend=python \\\n--repoagent=checksum \\\n--endpoint=grpc \\\n--endpoint=http \\\n--extra-backend-cmake-arg=tensorflow:TRITON_TENSORFLOW_INSTALL_EXTRA_DEPS=ON\n\ngpu版本的构建命令如下:\n./build.py -v --enable-logging --enable-stats --enable-metrics --enable-gpu-metrics \\\n--enable-tracing --enable-nvtx --enable-gpu --backend=tensorflow1 --backend=tensorflow2 \\\n--repoagent=checksum --endpoint=grpc --endpoint=http\n\n事实上,如果要在内部维护triton,直接使用build.py是远远不够的,服务需要一定的改造来适应现有的基础设施。通常我们需要这些dockerfile文件:\n\nbase.Dockerfile 安装了各种三方库的镜像文件\nbuild.Dockerfile 继承base.Dockerfile,并安装了common、core、backend库的镜像文件\nxxx_backend.Dockerfile 继承build.Dockerfile,用于编译各个backend的镜像\nserver.Dockerfile 构建server,并融合各个backend的镜像文件\n\n除了镜像外,需要将common、core、backend、server的代码放到代码库中,根据不同的Dockerfile来编译不同的代码。不同的backend放到不同的代码库中,一般还是会改一改的。\n服务配置todo: 服务配置说明\n模型配置triton支持多种文件存储,包括s3、as、gs和本地存储,通常能满足实际需要。\ntriton的模型目录要求如下:\n\n模型数据根目录\nA模型目录\nconfig.pbtxt文件\n版本A1目录\n各种模型的数据文件\n\n\n版本A2目录\n…\n\n\nB模型目录\nconfig.pbtxt文件\n版本B1目录\n版本B2目录\n…\n\n\n\n\n\nconfig.pbtxt是模型的配置文件。在triton中一个模型可以拥有多个版本,但是输入输出都是固定的,并且使用的引擎也是固定的,即一个模型可以有多个版本,一个引擎可以有多个模型。\n模型的具体配置可以在triton的model_repository文档里看到,这里简单介绍一下。\n模型的配置分为必填配置和选填配置两种:\n\n必填配置\n模型名、推理引擎、引擎库路径\n响应模式(一对一还是一对多)\nbatch容量\n模型的输入输出定义\n\n\n选填配置\n版本控制策略\n模型实例分组策略\n调度器策略\n优化策略\n预热配置\n\n\n\ntodo: 展开介绍\n模型管理通常公司会有一个模型管理平台,triton模型加载机制需要与这个平台适配,从而能够更好的管理。当服务的实例较多时,直接从s3、as、gs或者hdfs上获取模型的方式,可能不太适用。毕竟这些对象存储的作用是存储数据,而不是支撑大流量读取。这时,需要基于p2p搭建一套模型订阅推送的机制,保障模型正常发布的同时,减轻集中存储的压力。\n要实现这个功能,通常每个Node上部署一个p2p实例pod,同时进行目录挂载,将模型目录分别挂载到triton的pod和p2p的pod。\ntriton在服务启动时根据配置向平台注册,并获取到需要的模型机器版本。然后triton与p2p的pod交互,让其拉去相应的模型数据,并获得拉去任务的状态,进行模型加载。并且定时上传心跳及从平台获取模型的最新版本情况,以此决定是否需要拉去最新的版本。\np2p的服务可以基于dragonfly或者bbts改造,模型平台也需要提供获取模型版本信息的接口。triton扫目录获取模型版本的逻辑也需要改成从模型平台接口获取。\n自定义后端todo:\n源码阅读todo:\n","categories":["inference"],"tags":["serving","triton","tensorRT"]},{"title":"node-addon-api使用文档","url":"/posts/10f6a378.html","content":"简介“node-addon-api”是nodejs的”n-api”接口的c++封装, 通过提供c++对象模型和异常异常处理方式, 简化nodejs开发的成本.\n“n-api”是nodejs为原生拓展提供的c语言风格ABI, 它独立于js运行环境, 旨在屏蔽js运行环境的差异, 让拓展能够运行在不同版本的nodejs下.\n“node-addon-api”不作为nodejs的组件发布, 它只是基于nodejs的”n-api”, 这样, “node-addon-api”将于nodejs本身解耦, 基于”node-addon-api”的拓展运行在新nodejs环境时, 不需要重新编译.\n值得注意的是, nodejs的其他接口, 例如libuv等, 并不包含在nodejs的拓展ABI内, 基于这些接口开发的nodejs拓展无法保证在多个版本的nodejs环境中运行. 由于”n-api”是nodejs 6.x之后才出现的, 因此这套ABI只支持nodejs6.x之后的版本. \n当新的api被添加到n-api后, node-addon-api必须马上更新, 否则node-addon-api就无法使用这部分特性.\n本文将数据结构, 异常处理, 与js交互等多个方面介绍node-addon-api.\n数据结构首先是node-addon-api的数据结构, node-addon-api的数据结构是封装的n-api的, 我把它的数据结构分为两类, 一类是js中有直接对应类型的, 另一类是js中没有直接对应的类型, 但node-addon-api中出现的.\n基本类型\n\n\njs类型\nc++类\n说明\n\n\n\nstring\nString\n字符串, 使用unicode存储\n\n\nnumber\nNumber\n数字, 使用浮点存储\n\n\nboolean\nBoolean\n布尔值\n\n\nbigInt\nBigInt\n大整形, 新引入, 使用uint64数组存储\n\n\nobject\nObject\n对象\n\n\nsymbol\nSymbol\n符号\n\n\nBuffer\nBuffer\n二进制类型, 不受gc管理\n\n\nfunction\nFunction\n函数类型\n\n\narrayBuffer\nArrayBuffer\nbuffer数组类型\n\n\ntypedArray\nTypedArray\n类型数组\n\n\nDataView\nDataView\n视图\n\n\npromises\nPromises\n\n\n\n特殊类型\n\n\n特殊类型\n说明\n\n\n\nName\n特殊的类型, 可以用来作为类的属性名, 支持字符串和symbol\n\n\nEnv\n特殊结构, 包含了当前请求的运行环境\n\n\nValue\njs类型的c++表现类型, js类型的基类\n\n\nCallbackInfo\n特殊结构, 包含函数参数列表和Env对象, 常出现在c++实现的js函数参数表中, , 由nodejs环境生成传递给自定义函数.\n\n\nReference\n特殊引用类型, 类似于c++中的shared_ptr, 创建时不添加计数器. 当计数器为0时, 不负责删除数据, 使用垃圾回收机制回收数据.\n\n\nExternal\n用来包装c++数据的结构, 方便用户管理自定义结构, 这个类提供自定义清理函数接口, 可以让用户制定清理方法.\n\n\nObjectReference\n对象引用类型, 是Reference的子类, 包含引用对象和一个计数器, 相当于shared_ptr.\n\n\nPropertyDescriptor\njs对象的属性描述, 可以是函数/变量/访问控制等.\n\n\nFunctionReference\n\n\n\nObjectWrap\n\n\n\nClassPropertyDescriptor\n\n\n\n\n\n\n\n\n\n\n\n对象及引用与js交互自定义函数自定义类js函数调用异步操作异常处理内存管理生命周期额外类型其他版本管理异常处理内存管理对象生命周期管理异步操作promises版本管理","categories":["nodejs"]},{"title":"node-addon-api-example","url":"/posts/ebe76269.html","content":"","categories":["nodejs"]},{"title":"nodejs_ext","url":"/posts/58b03904.html","content":"","categories":["nodejs"]},{"title":"v8_learn","url":"/posts/c7657d28.html","content":"","categories":["nodejs"]},{"title":"v8嵌入式开发--v8js篇","url":"/posts/137637df.html","content":"简介v8js是一个特殊的php拓展, 其作用是将v8嵌入到php中, 使得用户可以在php中运行js代码. 同时, 经过作者的努力, 运行在php中的js可以无缝访问并php中的数据结构, 调用php內建的函数, 从而实现”1 + 1 > 2”的目标. 本文将跟随作者的, 领略v8js的风采.\n功能介绍使用一个工具是了解这个工具的最好方式, 笔者将在这一节中介绍v8js拓展的功能及使用.\n首先介绍一下v8js的提供的接口, v8js的接口如下:\n<?php\nclass V8Js\n{\n /* Constants */\n\n const V8_VERSION = '';\n\n const FLAG_NONE = 1;\n const FLAG_FORCE_ARRAY = 2;\n const FLAG_PROPAGATE_PHP_EXCEPTIONS = 4;\n\n /* Methods */\n\n /**\n * Initializes and starts V8 engine and returns new V8Js object with it's own V8 context.\n * @param string $object_name\n * @param array $variables\n * @param array $extensions\n * @param bool $report_uncaught_exceptions\n * @param string $snapshot_blob\n */\n public function __construct($object_name = \"PHP\", array $variables = [], array $extensions = [], $report_uncaught_exceptions = TRUE, $snapshot_blob = NULL)\n {}\n\n /**\n * Provide a function or method to be used to load required modules. This can be any valid PHP callable.\n * The loader function will receive the normalised module path and should return Javascript code to be executed.\n * @param callable $loader\n */\n public function setModuleLoader(callable $loader)\n {}\n\n /**\n * Provide a function or method to be used to normalise module paths. This can be any valid PHP callable.\n * This can be used in combination with setModuleLoader to influence normalisation of the module path (which\n * is normally done by V8Js itself but can be overriden this way).\n * The normaliser function will receive the base path of the current module (if any; otherwise an empty string)\n * and the literate string provided to the require method and should return an array of two strings (the new\n * module base path as well as the normalised name). Both are joined by a '/' and then passed on to the\n * module loader (unless the module was cached before).\n * @param callable $normaliser\n */\n public function setModuleNormaliser(callable $normaliser)\n {}\n\n /**\n * Compiles and executes script in object's context with optional identifier string.\n * A time limit (milliseconds) and/or memory limit (bytes) can be provided to restrict execution. These options will throw a V8JsTimeLimitException or V8JsMemoryLimitException.\n * @param string $script\n * @param string $identifier\n * @param int $flags\n * @param int $time_limit in milliseconds\n * @param int $memory_limit in bytes\n * @return mixed\n */\n public function executeString($script, $identifier = '', $flags = V8Js::FLAG_NONE, $time_limit = 0, $memory_limit = 0)\n {}\n\n /**\n * Compiles a script in object's context with optional identifier string.\n * @param $script\n * @param string $identifier\n * @return resource\n */\n public function compileString($script, $identifier = '')\n {}\n\n /**\n * Executes a precompiled script in object's context.\n * A time limit (milliseconds) and/or memory limit (bytes) can be provided to restrict execution. These options will throw a V8JsTimeLimitException or V8JsMemoryLimitException.\n * @param resource $script\n * @param int $flags\n * @param int $time_limit\n * @param int $memory_limit\n */\n public function executeScript($script, $flags = V8Js::FLAG_NONE, $time_limit = 0 , $memory_limit = 0)\n {}\n\n /**\n * Set the time limit (in milliseconds) for this V8Js object\n * works similar to the set_time_limit php\n * @param int $limit\n */\n public function setTimeLimit($limit)\n {}\n\n /**\n * Set the memory limit (in bytes) for this V8Js object\n * @param int $limit\n */\n public function setMemoryLimit($limit)\n {}\n\n /**\n * Set the average object size (in bytes) for this V8Js object.\n * V8's \"amount of external memory\" is adjusted by this value for every exported object. V8 triggers a garbage collection once this totals to 192 MB.\n * @param int $average_object_size\n */\n public function setAverageObjectSize($average_object_size)\n {}\n\n /**\n * Returns uncaught pending exception or null if there is no pending exception.\n * @return V8JsScriptException|null\n */\n public function getPendingException()\n {}\n\n /**\n * Clears the uncaught pending exception\n */\n public function clearPendingException()\n {}\n\n /** Static methods **/\n\n /**\n * Registers persistent context independent global Javascript extension.\n * NOTE! These extensions exist until PHP is shutdown and they need to be registered before V8 is initialized.\n * For best performance V8 is initialized only once per process thus this call has to be done before any V8Js objects are created!\n * @param string $extension_name\n * @param string $code\n * @param array $dependencies\n * @param bool $auto_enable\n * @return bool\n */\n public static function registerExtension($extension_name, $code, array $dependencies, $auto_enable = FALSE)\n {}\n\n /**\n * Returns extensions successfully registered with V8Js::registerExtension().\n * @return array|string[]\n */\n public static function getExtensions()\n {}\n\n /**\n * Creates a custom V8 heap snapshot with the provided JavaScript source embedded.\n * Snapshots are supported by V8 4.3.7 and higher. For older versions of V8 this\n * extension doesn't provide this method.\n * @param string $embed_source\n * @return string|false\n */\n public static function createSnapshot($embed_source)\n {}\n}\n\nfinal class V8JsScriptException extends Exception\n{\n /**\n * @return string\n */\n final public function getJsFileName( ) {}\n\n /**\n * @return int\n */\n final public function getJsLineNumber( ) {}\n /**\n * @return int\n */\n final public function getJsStartColumn( ) {}\n /**\n * @return int\n */\n final public function getJsEndColumn( ) {}\n\n /**\n * @return string\n */\n final public function getJsSourceLine( ) {}\n /**\n * @return string\n */\n final public function getJsTrace( ) {}\n}\n\nfinal class V8JsTimeLimitException extends Exception\n{\n}\n\nfinal class V8JsMemoryLimitException extends Exception\n{\n}\n\n\n\n\n\n从上面接口的注释可以知道, 使用v8js拓展执行js代码时, 基本流程是先创建一个V8Js类的对象, 然后执行成员函数executeString, 函数返回的结果即是js的执行结果.\nV8js的成员函数setTimeLimit和setMemoryLimit函数用于设置js执行的时间限制和内存限制.\n使用例子后续补充.\n架构设计接着讲一讲v8js拓展的架构设计. v8js模块包含了一个计时器线程, 计时任务双线队列, 全局变量结构体. 在全局变量结构体中存储了v8的platform. v8js模块中实现了一个名为V8Js的类, 在这个类的构造函数中, v8js会出现一个isolate对象, 并创建一个全局上下文. 那么v8js模块中, 一个V8Js类的对象包含一个独立的isolate对象. 整体架构如下图所示:\n详细设计js代码执行v8的源码中提供了一个hello的代码, 如下所示:\n// 初始化周边数据\nv8::V8::InitializeICUDefaultLocation(argv[0]);\nv8::V8::InitializeExternalStartupData(argv[0]);\n\n// 创建platform\nstd::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();\n\n// 初始化V8环境\nv8::V8::InitializePlatform(platform.get());\nv8::V8::Initialize();\n\n// 构造参数\nv8::Isolate::CreateParams create_params;\ncreate_params.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator();\n\n// 创建isolate对象(v8虚拟机实例)\nv8::Isolate* isolate = v8::Isolate::New(create_params);\nv8::Isolate::Scope isolate_scope(isolate);\nv8::HandleScope handle_scope(isolate);\n\n// 创建上下文\nv8::Local<v8::Context> context = v8::Context::New(isolate);\nv8::Context::Scope context_scope(context);\n\n// 将普通字符串转化为V8的字符串\nv8::Local<v8::String> source = v8::String::NewFromUtf8(isolate, "'Hello' + ', World!'", v8::NewStringType::kNormal).ToLocalChecked();\n\n// 编译\nv8::Local<v8::Script> script = v8::Script::Compile(context, source).ToLocalChecked();\n\n// 执行\nv8::Local<v8::Value> result = script->Run(context).ToLocalChecked();\n\n// 执行结果转化为普通字符串\nv8::String::Utf8Value utf8(isolate, result);\nprintf("%s\\n", *utf8);\n\n// 销毁isolate\nisolate->Dispose();\n\n// 销毁V8环境和platform\nv8::V8::Dispose();\nv8::V8::ShutdownPlatform();\ndelete create_params.array_buffer_allocator;\n从demo可以看到, 运行一个js脚本需要的操作包括:\n\n创建platform\n创建isolate\n创建context\n字符串编译成script\n运行script\n取出结果\n销毁环境\n\nv8js拓展将platform创建放在了第一个V8Js类的构造函数中; isolate和context的创建放在了V8Js的构造函数. executeString中执行了编译和运行script的操作. 对象析构的时候销毁isolate对象. 模块退出的时候销毁platform等操作.\n內建函数实现在这个部分需要介绍一下v8的template机制以及v8各个元素之间的关系. 下面是v8的常用名词:\n\nplatform 平台, 一个进程中可以有多个, 但是只有一个生效\nisolate v8虚拟机, 可以存在多个, 一个isolate同一时间只允许一个线程访问\ncontext 运行上下文, 可以有多个, 可以嵌套, 从属于isolate. \nscope 作用域, isolate和context都有scope, 用于垃圾回收处理\ntemplate 模板, 用于创建函数和对象, 从属于isolate\n\n在v8中, 对象和函数从属于context, 而context在isolate的scope销毁时会被一同销毁, 那么这些对象和函数需要在context创建的时候被不停的创建, 为了省去这部分工作量, v8引入了template, 用于创建对象和函数.\n內建函数的实现就是基于template实现的. 首先V8Js的的构造函数中会创建一个template对象, 然后再这个对象中添加functionTemplate对象, 而这些functionTemplate对象封装了內建函数. 在contenxt创建的时候, 将这个template对象作为实参传入, 那么创建出来的context就包含了內建函数.\ntemplate包含ObjectTemplate和FunctionTemplate两种, 前者创建对象, 后者创建函数. 如果用户想创建一个类呢? 由于js早期并不存在class关键词, 创建一个类的对象都是通过函数实现的. 所以创建一个类需要使用的是FunctionTemplate.\nv8js拓展的內建函数的具体实现在文件”v8js_methods.cc”中, 在v8js_register_methods函数中将这些內建函数添加到template对象中. 这个函数在V8Js类的构造函数中被调用, 位置在contenxt被创建之前.\ncommonjs模块实现首先讲一下什么是commonjs模块规范. “规范”认为, 每个文件是一个模块(module), 并拥有其自己的作用域. 在这个作用域内声明的变量和函数都是这个作用域私有的. 模块信息存储在module对象中, module提供exports对象用于暴露作用域内的函数/变量. 模块之间通过require函数加载其他的模块.\n为了实现上面规范, 在每个文件的作用域内, 需要提前声明module, exports对象和require函数. 并且module和exports对象存在于模块中, 模块间的module和exports都不同. 参考上文中的template的功能, module和exports可以实现为两个ObjectTemplate, 并注册到主ObjectTemplate. 同样, require也可以类似实现. 这时基于主ObjectTemplate创建context, 并在这个context运行指定文件, 并提取exports, 即可暴露指定的接口(变量和函数).\n考虑到重新创建context成本较大, v8的context支持基于老context创建新的context, 但是基于这种方式就无法使用到template了. nodejs的解决方案是, 创建一个函数, 在函数体中添加模块的代码, 将module/exports/require作为形参传入到. 例如, 模块的代码如下:\nvar x = 5;\nvar addX = function (value) {\n return value + x;\n};\nmodule.exports.x = x;\nmodule.exports.addX = addX;\n\nnodejs编译编译这个代码之前, 将这个模块的代码处理为:\n(function(module, exports, require) {\n var x = 5;\n var addX = function (value) {\n return value + x;\n };\n module.exports.x = x;\n module.exports.addX = addX;\n});\n然后, 创建module对象, exports对象, reuqire函数, 将这三个作为实参传入到生成的函数中去, 最后提取module和exports对象缓存起来. 当其他的模块reuqire这个模块时, 先查缓存, 命中缓存则直接返回exports对象.v8js借鉴了nodejs的方式实现了一个commonjs模块.由于reuqire一个模块时, 可以指定模块的相对路径, 这就要求reuqire在被调用时能够知道调用reuqire的文件的绝对路径. v8创建函数时支持给函数传递一个meta数据, v8js利用这个特性给require传递了路径数据. “v8js_commonjs.cc”中实现了一套相对路径推算绝对路径的代码, 使用c++17或者boost的话, 可以使用filesystem的canonical函数解决.\n超限功能实现前文提到v8js拓展包含一个计时器线程和计时任务队列, v8js依靠它们实现了超限功能.V8Js类的对象在执行js脚本时, v8js会基于isolate/context/超时配置/内存限制创建一个超限任务, 并添加到计时任务队列中.计时器线程会取出队列中的任务, 检查是否超时, 内存是否超限. 对超限的任务, 超时线程将终止任务运行. 在判断内存超限时, 计时器线程需要先解锁isolate, 然后获取堆统计信息, 如果超限就手工gc并终止当前任务.\njs中调用php数据实现这部分略\n改进与优化isolate归属问题在v8js中isolate是V8Js对象级别的, 事实上isolate和全局context的创建成本较大, 并不适合放在对象级别. 基于php单线程运行的特点, isolate和全局context可以存放在线程级别中. 每次执行脚本时, 基于全局context创建新的context.\n学习总结\n学习了js脚本运行的流程\n学习了v8中各个名词之间的关系\n学习了多isolate的使用\n\n","categories":["nodejs"],"tags":["v8","js","javascript","nodejs"]}]