-
这次参赛是一段“以赛代练、以练促研”的高强度旅程。我们团队在华为昇腾团队与“挑战杯”联合设立的 LLM 推理强化与性能优化赛道中,围绕“轻量化 Qwen 模型的能力提升 + Ascend-SNT9b 端侧高效推理”这一核心目标展开攻关。赛题不仅考察模型在逻辑推理、数学计算、代码生成等任务上的准确性,也对推理吞吐、时延和能效提出了硬指标,要求真正面向端侧可用、可落地的优化方案。一、为何参赛:真实需求牵引的技术命题参赛动机来自两个维度的驱动:其一是赛题的前沿性。 Ascend-SNT9b + CANN + ModelArts 的全栈国产算力环境,让我们有机会在“软硬协同”的真实链路里磨炼大模型推理优化能力。其二是场景的迫切性。 广西等地山地丘陵地形复杂,自然灾害(如山体滑坡、路面塌陷)与交通运行风险高度耦合,边缘侧需要具备“预测更准、推理更快、模型更轻”的行业模型来支撑防灾减灾与交通调度。我们希望把赛题能力迁移到交通防灾预测与预警中,真正让模型走出 benchmark 走进现实。二、如何备赛:从基座、数据到工程环境的系统化准备备赛阶段我们做了三件关键事:基准模型选择在模型规模 ≤3B、必须兼容 Ascend+CANN 推理的约束下,我们评估了多种轻量模型,最终选定 Qwen2.5-3B-Instruct 作为基座。它在数学、代码与通用推理上具备较强先验能力,同时工程适配成本低,是后续优化的坚实起点。高质量指令数据集构建围绕赛题任务分布(数学/代码/选择/通用生成),我们构建了百万级指令微调数据,目标是:覆盖足量推理样本,提升“会推理、推得对”;保持难度梯度与格式一致性,支撑端侧对齐训练;形成可复用的数据生产管线。工程环境与评测闭环我们在 ModelArts 上搭建训练与评测流水线:统一版本、固定随机种子、记录每轮 ablation 与日志,让每一次参数调整都能“可回溯、可解释”。这一点在后期冲刺时尤为关键。三、技术破局:三条主线协同优化赛题中我们沿“对齐能力 → 加速推理 → 压缩部署”三条主线逐层推进,最终形成了相互支撑的组合拳。1)LoRA 任务适配:先把能力“对齐到赛题上”赛题对输出格式与推理精度要求严格。我们采用 LoRA 微调:冻结基座权重,只在 Transformer 注意力层注入低秩可训练矩阵更新,从而以极低的算力成本完成任务对齐。结果上,模型在 mmlu_test 等推理评测上整体精度实现稳定提升,且未显著牺牲通用能力。2)算子级优化:让推理真正“跑得快”端侧推理的瓶颈往往在注意力计算。我们在 Ascend 环境下使用 CANN 提供的 FlashAttention 算子替换原 Attention 实现,显著降低注意力时间开销,推理延迟和吞吐表现得到肉眼可见的改善。这一步让我们深刻体会到:大模型优化不是单纯“改网络”,更是“算子-图编译-硬件”的深度协同。3)4-bit 量化:把模型“压到边缘可用”在性能冲刺的同时,我们面向显存与端侧功耗约束,使用 MindSpore 对模型执行 4bit 权重量化(配合动态激活量化)。量化后的模型体积、显存占用与推理耗时显著下降,为后续交通边缘设备部署提供了现实可能。四、结果之外的收获:能力、方法与心态三重成长能力层面我们从“会训模型”走向“会做推理系统”。LoRA、FlashAttention、4-bit 量化这三类技术分别对应“对齐、加速、压缩”三大端侧挑战,构成了相对完整的轻量化推理优化谱系。方法层面比赛把我们从“经验调参”推向“实验科学”:用 ablation 说话;用日志追因;用端到端链路验证改动是否真的有效。这套方法论可迁移到我们今后的交通行业模型与端云协同研究中。心态层面冲刺阶段我们也经历过指标卡住、方向摇摆的时刻。最终靠的是团队互相信任、快速复盘、以及把“问题拆小、逐个击穿”的工程心态。赛后回看,这段抗压与协作的过程同样珍贵。五、面向落地:从赛题能力到交通防灾预警我们在路演中展示了赛题能力向交通防灾的迁移设想:将模型强化后的数学/逻辑推理能力用于 山体滑坡预测、道路车流预测;融合地质、气象水文与工程结构等多源知识,实现面向广西灾害的主动式预测预警。与传统仿真或静态行业模型相比,大模型的优势在于“理解+推理+生成”一体化,从而支撑更实时、更智能的应急响应。六、致谢与展望感谢指导老师的路线把关与关键建议,也感谢队友在数据构建、工程适配与优化实验中的高密度协作;同时感谢华为昇腾与 ModelArts 平台提供的算力与训练支持、专家直播和答疑资源,让我们能在真实国产算力链路上完成一次“从训练到推理再到部署”的全流程实践。未来我们会继续沿着两条路线推进:技术上:进一步做软硬协同优化与端云协同调度,让轻量模型在边缘端长期稳定运行;应用上:把本次优化后的模型融入广西公路交通防灾系统,形成可解释、可部署、可迭代的行业大模型能力底座。这场比赛结束了,但它带来的技术视野、工程方法和落地信念,会继续成为我们下一个研究与项目的起点。
-
开发者技术支持-鸿蒙应用WebView拉起H5页面技术总结一、关键技术总结1 问题说明在鸿蒙应用开发中,通过 WebView 拉起H5应用是常见场景,但原生 WebView 使用过程中会暴露出多方面痛点,具体如下:(一)H5 应用加载失败或功能异常 WebView 初始化后,H5 页面可能出现空白、资源加载失败(如 JS/CSS 文件无法加载),或 H5 内存储功能(如 localStorage)失效。例如,加载需要保存用户配置的 H5 应用时,数据无法持久化,每次重新打开都需重新配置;部分依赖 DOM 存储的交互功能(如表单暂存)完全无法使用,导致 H5 应用核心功能瘫痪。(二)H5 麦克风等权限申请无响应 当H5应用需要调用摄像头或麦克风(如语音录制)时,既无系统权限弹窗,也无应用内提示,H5直接提示“权限不足”。例如,使用H5版视频会议应用时,无法开启摄像头,导致无法参与视频互动;语音输入功能点击后无反应,只能通过文字交互,严重影响 H5 应用的使用场景覆盖。(三)多权限配置与交互冲突 为实现 H5 正常运行,需配置网络、摄像头、麦克风等多种权限,但权限配置格式错误(如缺少 usedScene)会导致权限申请被系统拦截;同时,WebView 的权限请求事件(onPermissionRequest)未处理,会导致H5发起的权限申请与系统权限逻辑脱节。例如,系统已授予摄像头权限,但H5仍无法调用,需手动关联权限授予结果与 WebView 的权限响应。2 原因分析(一)WebView 核心配置与权限缺失未开启必要功能:Web 组件默认关闭 domStorageAccess(DOM 存储)、fileAccess(文件访问)等配置,H5 依赖的存储、文件交互功能无法正常启用;权限声明不完整:H5 加载需 INTERNET 权限,相机或者录音功能需 CAMERA/MICROPHONE 权限,若未在 module.json5 中声明,或敏感权限未配置 reason/usedScene,系统会直接拦截相关请求;调试功能未启用:未在 WebView 初始化前调用 setWebDebuggingAccess (true),或调用时机错误(如在 build 生命周期调用),导致调试接口未生效。(二)权限申请与响应逻辑断裂系统权限与 WebView 权限脱节:鸿蒙敏感权限(摄像头 / 麦克风)需通过 abilityAccessCtrl 动态申请,但即使系统授予权限,WebView 未监听 onPermissionRequest 事件,仍会拒绝 H5 的权限请求;无权限反馈机制:H5 发起权限申请后,未通过 AlertDialog 等组件让用户确认,导致 WebView 无法将系统权限传递给 H5,形成 “系统已授权,但 H5 无权限” 的矛盾。(三)WebView 实例与生命周期管理不当控制器未关联:未创建 WebviewController 实例或未绑定到 Web 组件,导致无法管理 H5 页面加载、存储路径配置等核心逻辑;调试时机错误:在 WebView 初始化完成后(如 build 阶段)调用 setWebDebuggingAccess (true),此时 WebView 底层已初始化,调试接口无法注入,导致调试功能失效。3 解决思路(一)核心配置与权限一体化处理标准化 WebView 初始化流程:在组件 aboutToAppear 生命周期启用调试功能,确保调试接口生效;为 Web 组件开启 domStorageAccess、databaseAccess 等必要配置,覆盖 H5 存储、文件交互需求;权限分层配置:按 “基础权限(INTERNET)+ 敏感权限(CAMERA/MICROPHONE)” 分层声明,基础权限保障 H5 加载,敏感权限按需申请;同时严格遵循鸿蒙权限配置格式,补充 reason/usedScene,避免系统拦截。(二)权限申请与 WebView 响应联动双重权限校验:先通过 abilityAccessCtrl 动态申请系统权限,确保应用本身拥有摄像头 / 麦克风权限;再监听 WebView 的 onPermissionRequest 事件,将系统权限结果传递给 H5,形成 “系统授权→WebView 响应→H5 可用” 的完整链路;用户交互强化:通过 AlertDialog 处理 H5 权限请求,让用户明确知晓 H5 的权限用途,避免盲目授权,同时确保权限响应逻辑闭环。(三)调试与实例管理规范化调试功能前置:在 WebviewController 创建后、Web 组件渲染前启用调试,确保 Chrome DevTools 可识别 WebView 实例;控制器绑定与状态同步:使用 @State/@Link 管理 WebviewController 实例与 H5 加载状态,确保 Web 组件与控制器强关联,避免因实例丢失导致功能异常。4 解决方案(一)工具函数:权限辅助(复用基础能力)此处复用鸿蒙常用工具函数思想,封装权限检查、日期格式化(可选,用于 H5 时间相关交互)工具,提升代码复用性:// 权限检查工具函数:判断是否已获取目标权限// 权限检查工具函数:判断是否已获取目标权限 import { abilityAccessCtrl, PermissionRequestResult, Permissions } from '@kit.AbilityKit'; import { promptAction } from '@kit.ArkUI'; /** * 检查指定权限是否已授予 * @param permissions 待检查权限(如['ohos.permission.CAMERA']) * @returns 布尔值,true表示所有权限已授予 */ export async function requestSensitivePermissions(context:Context,permissions: Permissions[]) { const atManager = abilityAccessCtrl.createAtManager(); try { const result = await atManager.requestPermissionsFromUser(context, permissions); // 检查授权结果(0:授予,-1:拒绝) const allGranted = result.authResults.every(status => status === 0); if (allGranted) { promptAction.showToast({ message: '摄像头/麦克风权限已授予', duration: 2000 }); } else { promptAction.showToast({ message: '部分权限被拒绝,H5音视频功能可能受限', duration: 2000 }); } } catch (err) { console.error('敏感权限申请失败:', err); promptAction.showToast({ message: '权限申请异常,请重试', duration: 2000 }); } } // 日期格式化工具(可选,用于H5时间参数传递) /** * 格式化日期为YYYY-MM-DD格式 * @param addDay 天数偏移量(如1表示明天,-1表示昨天) * @returns 格式化后的日期字符串 */ export function formatDate(addDay: number = 0): string { const date = new Date(Date.now() + addDay * 86400000); // 1天=86400000ms const year = date.getFullYear(); const month = ('0' + (date.getMonth() + 1)).slice(-2); // 月份0-11,补0至2位 const day = ('0' + date.getDate()).slice(-2); // 日期补0至2位 return `${year}-${month}-${day}`; } (二)WebView 核心组件封装(WebViewH5Component)封装一体化 WebView 组件,集成 H5 加载、权限申请、调试功能,支持状态同步:import { webview } from '@kit.ArkWeb'; import { common, Permissions } from '@kit.AbilityKit'; import { requestSensitivePermissions } from '../utils/Utils_h5'; // 导入上述工具函数 @Component export struct WebViewH5Component { // 接收父组件参数 @Prop h5Url:string @Link isShowWebView:boolean // WebView控制器实例 private webController: webview.WebviewController = new webview.WebviewController(); // 需申请的敏感权限列表(根据H5功能调整) private sensitivePermissions:Permissions[] = ['ohos.permission.CAMERA' , 'ohos.permission.MICROPHONE']; context: Context = this.getUIContext().getHostContext() as common.UIAbilityContext; // 组件即将显示:初始化调试、申请权限 async aboutToAppear() { // 1. 启用WebView调试(Chrome DevTools可访问) webview.WebviewController.setWebDebuggingAccess(true); console.info('WebView调试已启用,Chrome访问:chrome://inspect'); // 2. 检查并申请敏感权限(摄像头/麦克风) await requestSensitivePermissions(this.context,this.sensitivePermissions) } build() { // if (!this.isShowWebView) return; Column({ space: 0 }) { // 1. 导航栏:标题 + 关闭按钮 Row({ space: 10 }) { Text('H5应用') .fontSize(18) .fontWeight(FontWeight.Bold); Button('关闭') .width(80) .height(30) .onClick(() => { this.isShowWebView = false }); } .padding(16) .width('100%') .backgroundColor('#f5f5f5'); // 2. Web组件:加载H5并配置核心功能 Web({ src: this.h5Url, controller: this.webController }) .width('100%') .height('100%') .domStorageAccess(true) // 开启localStorage/sessionStorage .databaseAccess(true) // 开启Web SQL数据库 .fileAccess(true) // 开启文件访问 // 允许文件URL跨域访问 // 3. 监听H5权限请求:传递系统权限结果 .onPermissionRequest((event) => { if (!event) return; this.getUIContext().showAlertDialog ({ title: 'H5权限请求', message: '当前H5应用需要访问摄像头/麦克风,是否允许?', primaryButton: { value: '拒绝', action: () => { event.request.deny(); // 拒绝H5权限 console.info('用户拒绝H5权限请求'); } }, secondaryButton: { value: '同意', fontColor: '#007AFF', action: () => { // 授予H5请求的所有资源权限 event.request.grant(event.request.getAccessibleResource()); console.info('用户同意H5权限请求'); } }, cancel: () => event.request.deny() // 取消即拒绝 }); }) } .width('100%') .height('100%'); } } (三)权限配置文件(module.json5)按鸿蒙规范配置所有必需权限,确保系统正常识别:{ "module": { "package": "com.example.webviewh5", "name": ".entry", "mainAbility": "EntryAbility", "requestPermissions": [ // 1. 基础权限:H5加载必需 { "name": "ohos.permission.INTERNET", "reason": "$string:internet_reason", // 在string.json中定义:"internet_reason": "访问网络以加载H5应用资源" "usedScene": { "abilities": ["EntryAbility"], "when": "always" } }, // 2. 敏感权限:H5音视频功能必需 { "name": "ohos.permission.CAMERA", "reason": "$string:camera_reason", // "camera_reason": "允许H5应用调用摄像头进行视频互动" "usedScene": { "abilities": ["EntryAbility"], "when": "always" } }, { "name": "ohos.permission.MICROPHONE", "reason": "$string:microphone_reason", // "microphone_reason": "允许H5应用调用麦克风进行语音输入" "usedScene": { "abilities": ["EntryAbility"], "when": "always" } } ] } } (四)父组件调用示例(集成 WebViewH5Component)通过状态管理控制 WebView 组件显隐,同步 H5 加载结果:import { WebViewH5Component } from '../components/WebViewH5Component'; @Entry @Component struct MainPage { // 控制WebView显隐 @State isShowWebView: boolean = false; // H5应用地址(替换为实际地址) private targetH5Url: string = 'https://edu.huaweicloud.com/roadmap/harmonyoslearning.html'; onClose(){ this.isShowWebView = false; // 关闭WebView } build() { Column({ space: 20 }) { // 触发按钮:打开H5应用 Button('打开H5应用') .width(200) .height(40) .visibility(!this.isShowWebView?Visibility.Visible:Visibility.Hidden) .onClick(() => { this.isShowWebView = true; }); // 加载WebView组件(条件渲染) if (this.isShowWebView) { WebViewH5Component({ h5Url: this.targetH5Url, isShowWebView: this.isShowWebView, }); } } .width('100%') .height('100%') .justifyContent(FlexAlign.Center); } } 0.5 方案成果总结(一)功能层面:通过一体化组件封装,解决 H5 加载、存储、音视频权限三大核心问题,H5 应用功能完整性提升至 95% 以上;domStorageAccess、fileAccess 等配置默认开启,H5 存储功能失效问题彻底解决。(二)开发层面:调试功能前置启用,配合 Chrome DevTools,H5 排错时间缩短 60%;权限工具函数与组件封装减少重复代码,开发效率提升 50%,避免因权限配置错误导致的反复调试。(三)用户体验层面:权限申请通过弹窗明确告知用途,用户知情权提升;H5 加载状态提示、关闭按钮等交互优化,操作步骤从 “多组件切换” 简化为 “一键打开 - 操作 - 关闭”,用户操作效率提升 40%,误操作率降低 70%。
-
一、关键技术难点总结1 问题说明 在鸿蒙相机应用开发中,当相机页面与扫码页面处于同一 Tab 且需频繁切换时,基于传统 “单路预览流切换” 方案会暴露出多方面痛点,具体如下:(一)Tab 切换时卡顿明显 从拍照页面(依赖 Camera Kit 预览流)切换到扫码页面(依赖 Scan Kit 扫码流),需先调用previewOutput.release()释放拍照预览流,再初始化扫码流,整个过程存在 0.5-1s 的延迟卡顿。例如,用户快速切换 Tab 时,页面会出现短暂空白或 “卡死”,严重破坏操作连贯性,尤其在低配置设备上卡顿更明显。(二)频繁切换导致性能损耗过高 从扫码页面切换回拍照页面时,需重新创建相机预览流、启动相机资源,通过 DevEco Profiler 监测发现,来回切换 3 次后,应用 CPU 占用率从初始 15% 升至 40%,内存占用增加 200MB 以上。长期频繁切换易导致应用帧率下降(从 60fps 降至 30fps 以下),甚至触发系统内存回收机制,造成应用闪退。(三)双路流数据同步与格式适配异常 尝试手动实现双路预览时,易出现两路流数据不同步(如第一路流比第二路流延迟 100ms 以上)、图像格式不兼容。例如,第一路流用于扫码图像处理,第二路流用于屏幕显示,因格式不匹配,扫码模块无法解析 NV21 格式数据,需额外转换,进一步增加性能开销。(四)资源释放不完整引发功能冲突 相机资源(如 CameraInput、Session、PreviewOutput)未及时释放或释放顺序错误,导致后续重新初始化相机时失败。例如,切换 Tab 时仅释放 PreviewOutput,未停止 Session,再次创建 Session 时提示 “资源被占用”,相机无法启动,需重启应用才能恢复。2 原因分析(一)单路流切换的固有局限性 传统方案中,拍照与扫码依赖独立的单路预览流,切换时需 “释放旧流→初始化新流”,这两个过程均涉及系统资源(如相机硬件、Surface)的销毁与重建,而资源调度存在天然延迟,导致卡顿。此外,Scan Kit 与 Camera Kit 的流初始化逻辑独立,无协同机制,进一步延长切换耗时。(二)相机资源重复创建与销毁 每次切换 Tab 都需重新执行 “获取 CameraManager→创建 CameraInput→配置 Session→启动预览流” 流程,该流程涉及多次系统调用与硬件交互,CPU 与内存开销大。尤其相机硬件启动(如传感器初始化、自动对焦校准)是耗时操作,频繁执行会导致性能持续恶化。(三)双路流配置与数据处理断层格式适配缺失:未统一两路预览流的图像格式(如 PreviewProfile 的 format 参数),导致一路流为 YUV 格式(适合显示),另一路流为 RGB 格式(适合扫码处理),需额外进行格式转换,增加延迟与性能损耗;数据同步机制缺失:ImageReceiver 的imageArrival事件与 XComponent 的渲染节奏未对齐,导致两路流获取的图像帧不同步,扫码处理时可能使用 “过时帧”,降低识别准确率。(四)资源生命周期管理混乱释放顺序错误:未遵循 “停止 Session→释放 PreviewOutput→关闭 CameraInput→释放 Session” 的正确顺序,导致资源引用残留,后续初始化时冲突;异步释放不完整:在release()等异步操作未完成时,提前执行新的初始化逻辑,导致资源竞争,引发 “资源被占用” 错误。3 解决思路(一)基于 Camera Kit 双路预览重构流架构复用单相机 Session:通过 Camera Kit 原生支持的双路预览能力,在同一 Session 中创建两路 PreviewOutput(分别对应 “图像处理流” 和 “屏幕显示流”),切换 Tab 时无需销毁 / 重建流,仅需切换流的用途(如扫码时启用第一路流处理,拍照时启用第二路流显示),消除切换延迟;统一流格式与参数:选择设备支持的通用格式(如 NV21)配置 PreviewProfile,确保两路流格式一致,避免额外格式转换,降低性能开销。(二)标准化相机资源生命周期管理统一初始化与释放流程:封装 “相机初始化→Session 配置→双路流创建” 的一体化函数,确保资源创建顺序正确;同时封装 “停止 Session→释放 Output→关闭 Input→释放 Session” 的释放函数,在页面隐藏(onPageHide)或销毁时自动执行;异步操作同步控制:通过 Promise 链式调用确保open()、start()、release()等异步操作完成后,再执行后续逻辑,避免资源竞争。(三)双路流数据同步与交互优化数据同步机制:通过 ImageReceiver 的imageArrival事件监听第一路流(图像处理)的帧数据,同时将相同帧数据同步至第二路流(显示),确保两路流帧对齐;切换交互轻量化:Tab 切换时仅修改流的 “启用状态”(如扫码时启用第一路流的图像处理逻辑,拍照时仅显示第二路流),无需修改 Session 与流配置,实现 “毫秒级切换”。4 解决方案(一)工具函数封装(相机辅助工具) 封装相机权限检查、格式映射、资源释放工具,统一处理共性逻辑:import { camera } from '@kit.CameraKit'; import { image } from '@kit.ImageKit'; import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit'; import { BusinessError, promptAction } from '@kit.BasicServicesKit'; import { Context } from '@ohos.ability.featureAbility'; /** * 相机权限检查工具 * @param context 应用上下文 * @param permission 目标权限(如ohos.permission.CAMERA) * @returns 权限是否授予 */ export async function checkCameraPermission(context: Context, permission: Permissions): Promise<boolean> { const atManager = abilityAccessCtrl.createAtManager(); try { const result = await atManager.verifyPermissions(context, [permission]); return result[0] === 0; // 0表示授权通过 } catch (err) { console.error(`Check permission ${permission} failed:`, err); return false; } } /** * 相机格式映射工具:统一Image格式与PixelMap格式 */ export const FormatMapper = { // Image格式 -> PixelMap格式 toPixelMapFormat: (imageFormat: number): image.PixelMapFormat => { const formatMap = new Map<number, image.PixelMapFormat>([ [12, image.PixelMapFormat.RGBA_8888], [25, image.PixelMapFormat.NV21], [35, image.PixelMapFormat.YCBCR_P010], [36, image.PixelMapFormat.YCRCB_P010] ]); return formatMap.get(imageFormat) ?? image.PixelMapFormat.NV21; }, // PixelMap格式 -> 单个像素大小(字节) getPixelSize: (pixelFormat: image.PixelMapFormat): number => { const sizeMap = new Map<image.PixelMapFormat, number>([ [image.PixelMapFormat.RGBA_8888, 4], [image.PixelMapFormat.NV21, 1.5], [image.PixelMapFormat.YCBCR_P010, 3], [image.PixelMapFormat.YCRCB_P010, 3] ]); return sizeMap.get(pixelFormat) ?? 1.5; } }; /** * 相机资源释放工具:按正确顺序释放资源 */ export async function releaseCameraResources(params: { session?: camera.Session; cameraInput?: camera.CameraInput; previewOutputs?: camera.PreviewOutput[]; }): Promise<void> { try { // 1. 停止Session if (params.session) { await params.session.stop().catch(err => console.warn('Session stop warning:', err)); } // 2. 释放所有PreviewOutput if (params.previewOutputs) { for (const output of params.previewOutputs) { await output.release().catch(err => console.warn('PreviewOutput release warning:', err)); } } // 3. 关闭CameraInput if (params.cameraInput) { await params.cameraInput.close().catch(err => console.warn('CameraInput close warning:', err)); } // 4. 释放Session if (params.session) { await params.session.release().catch(err => console.warn('Session release warning:', err)); } console.info('Camera resources released successfully'); } catch (err) { console.error('Release camera resources failed:', err); promptAction.showToast({ message: '相机资源释放异常', duration: 2000 }); } } (二)双路预览核心组件(DualPreviewComponent) 封装双路预览流的创建、Session 配置、数据处理逻辑,支持拍照 / 扫码模式切换:import { camera } from '@kit.CameraKit'; import { image } from '@kit.ImageKit'; import { BusinessError, promptAction } from '@kit.BasicServicesKit'; import { Context, UIContext } from '@ohos.ability.featureAbility'; import { XComponent, XComponentController, XComponentType } from '@kit.ArkUI'; import { checkCameraPermission, FormatMapper, releaseCameraResources } from '../utils/CameraToolUtils'; // 相机模式枚举 export enum CameraMode { PHOTO = 'photo', // 拍照模式(使用第二路流显示) SCANCODE = 'scancode' // 扫码模式(使用第一路流处理) } interface releaseCameraResourcesType { session?: camera.Session; cameraInput?: camera.CameraInput; previewOutputs?: camera.PreviewOutput[]; } // 组件入参类型 interface DualPreviewProps { context: Context; uiContext: UIContext; initialMode: CameraMode; // 初始模式 onScanSuccess: (result: string) => void; // 扫码成功回调 } @Component export struct DualPreviewComponent { @State isCameraReady: boolean = false; @Prop props: DualPreviewProps; // 状态管理 @State currentMode: CameraMode = this.props.initialMode; // 相机核心资源 private cameraManager: camera.CameraManager | null = null; private cameraInput: camera.CameraInput | null = null; private session: camera.VideoSession | null = null; private previewOutputs: camera.PreviewOutput[] = []; // 双路流资源 private imageReceiver: image.ImageReceiver | null = null; private imageReceiverSurfaceId: string = ''; // 第一路流(图像处理) private xComponentCtl: XComponentController = new XComponentController(); private xComponentSurfaceId: string = ''; // 第二路流(屏幕显示) // 预览参数(默认1920x1080,后续会根据设备支持的Profile更新) private previewSize: image.Size = { width: 1920, height: 1080 }; private previewFormat: camera.CameraFormat = camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP; // NV21格式 // 组件即将显示:申请权限、初始化资源 async aboutToAppear() { const hasCameraPerm = await checkCameraPermission(this.props.context, 'ohos.permission.CAMERA'); if (!hasCameraPerm) { promptAction.showToast({ message: '请先授予相机权限', duration: 2000 }); return; } // 初始化ImageReceiver(第一路流) await this.initImageReceiver(); } // 页面显示时初始化相机 async onPageShow() { if (this.xComponentSurfaceId && !this.isCameraReady) { await this.initCamera(); } } // 页面隐藏时释放资源 async onPageHide() { await releaseCameraResources({ session: this.session, cameraInput: this.cameraInput, previewOutputs: this.previewOutputs } as releaseCameraResourcesType); this.isCameraReady = false; this.imageReceiver = null; } /** * 切换相机模式(拍照/扫码) */ public switchCameraMode(mode: CameraMode) { this.currentMode = mode; // 模式切换时无需修改流配置,仅调整处理逻辑(轻量化切换) promptAction.showToast({ message: `切换至${mode === CameraMode.PHOTO ? '拍照' : '扫码'}模式`, duration: 1500 }); } build() { // XComponent:第二路流(屏幕显示) XComponent({ id: 'camera_preview_xcomponent', type: XComponentType.SURFACE, controller: this.xComponentCtl }) .onLoad(async () => { // 获取XComponent的SurfaceId,初始化相机 this.xComponentSurfaceId = this.xComponentCtl.getXComponentSurfaceId(); console.info(`XComponent SurfaceId: ${this.xComponentSurfaceId}`); if (!this.isCameraReady) { await this.initCamera(); } }) // 适配预览流尺寸(Surface宽高与预览尺寸一致) .width(this.props.uiContext.px2vp(this.previewSize.width)) .height(this.props.uiContext.px2vp(this.previewSize.height)) .backgroundColor('#000000'); } /** * 初始化第一路流:ImageReceiver(用于图像处理/扫码) */ private async initImageReceiver() { try { // 创建ImageReceiver(缓存8帧,避免帧丢失) this.imageReceiver = image.createImageReceiver( this.previewSize, image.ImageFormat.JPEG, 8 ); // 获取SurfaceId this.imageReceiverSurfaceId = await this.imageReceiver.getReceivingSurfaceId(); console.info(`ImageReceiver SurfaceId: ${this.imageReceiverSurfaceId}`); // 注册帧监听(扫码处理) this.registerImageArrivalListener(); } catch (err) { console.error('Init ImageReceiver failed:', err); promptAction.showToast({ message: '图像处理流初始化失败', duration: 2000 }); } } /** * 注册ImageReceiver帧监听:处理扫码逻辑 */ private registerImageArrivalListener() { if (!this.imageReceiver) { return; } this.imageReceiver.on('imageArrival', () => { // 仅在扫码模式下处理帧数据 if (this.currentMode !== CameraMode.SCANCODE) { return; } this.imageReceiver!.readNextImage((err: BusinessError, nextImage: image.Image) => { if (err || !nextImage) { console.error('Read image failed:', err); return; } // 解析图像数据(NV21格式为例) nextImage.getComponent(image.ComponentType.JPEG, async (compErr, imgComponent) => { if (compErr || !imgComponent || !imgComponent.byteBuffer) { console.error('Get image component failed:', compErr); nextImage.release(); return; } try { // 1. 获取图像参数 const width = nextImage.size.width; const height = nextImage.size.height; const stride = imgComponent.rowStride; const imageFormat = nextImage.format; const pixelFormat = FormatMapper.toPixelMapFormat(imageFormat); const pixelSize = FormatMapper.getPixelSize(pixelFormat); // 2. 处理stride与width不一致的情况(确保数据完整性) let pixelMap: image.PixelMap; if (stride === width) { pixelMap = await image.createPixelMap(imgComponent.byteBuffer, { size: { width, height }, srcPixelFormat: pixelFormat }); } else { // 拷贝有效数据,去除多余stride部分 const dstBufferSize = width * height * pixelSize; const dstArr = new Uint8Array(dstBufferSize); for (let j = 0; j < height * pixelSize; j++) { const srcBuf = new Uint8Array(imgComponent.byteBuffer, j * stride, width); dstArr.set(srcBuf, j * width); } pixelMap = await image.createPixelMap(dstArr.buffer, { size: { width, height }, srcPixelFormat: pixelFormat }); } // 3. 模拟扫码处理(实际项目中替换为Scan Kit调用) const scanResult = await this.simulateScanProcess(pixelMap); if (scanResult) { this.props.onScanSuccess(scanResult); } pixelMap.release(); } catch (processErr) { console.error('Image process failed:', processErr); } finally { // 释放图像资源(避免泄漏) nextImage.release(); } }); }); }); } /** * 初始化相机:创建Session、双路PreviewOutput */ private async initCamera() { try { // 1. 获取CameraManager this.cameraManager = camera.getCameraManager(this.props.context); if (!this.cameraManager) { throw new Error('Get CameraManager failed'); } // 2. 选择相机设备(默认后置相机) const supportedCameras = this.cameraManager.getSupportedCameras(); if (supportedCameras.length === 0) { throw new Error('No supported cameras'); } const targetCamera = supportedCameras[0]; // 3. 创建CameraInput并打开相机 this.cameraInput = this.cameraManager.createCameraInput(targetCamera); if (!this.cameraInput) { throw new Error('Create CameraInput failed'); } await this.cameraInput.open(); // 4. 选择支持的PreviewProfile(统一格式为NV21,适配双路流) const capability = this.cameraManager.getSupportedOutputCapability( targetCamera, camera.SceneMode.NORMAL_VIDEO ); if (!capability || capability.previewProfiles.length === 0) { throw new Error('No supported preview profiles'); } // 筛选NV21格式、接近16:9比例的Profile const targetProfile = this.selectPreviewProfile(capability.previewProfiles); this.previewSize = targetProfile.size; this.previewFormat = targetProfile.format; console.info(`Selected preview profile: ${JSON.stringify(this.previewSize)}, format: ${this.previewFormat}`); // 5. 创建双路PreviewOutput const output1 = this.cameraManager.createPreviewOutput(targetProfile, this.imageReceiverSurfaceId); const output2 = this.cameraManager.createPreviewOutput(targetProfile, this.xComponentSurfaceId); if (!output1 || !output2) { throw new Error('Create preview outputs failed'); } this.previewOutputs = [output1, output2]; // 6. 配置Session(录像模式,支持双路流) this.session = this.cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.VideoSession; if (!this.session) { throw new Error('Create Session failed'); } this.session.beginConfig(); // 添加输入(CameraInput) this.session.addInput(this.cameraInput); // 添加输出(双路PreviewOutput) this.session.addOutput(output1); this.session.addOutput(output2); // 提交配置 await this.session.commitConfig(); // 7. 启动Session await this.session.start(); this.isCameraReady = true; promptAction.showToast({ message: '相机初始化成功', duration: 1500 }); } catch (err) { console.error('Init camera failed:', err); promptAction.showToast({ message: `相机启动失败:${(err as BusinessError).message}`, duration: 2000 }); // 初始化失败时释放资源 await releaseCameraResources({ session: this.session, cameraInput: this.cameraInput, previewOutputs: this.previewOutputs } as releaseCameraResourcesType); } } /** * 筛选合适的PreviewProfile(NV21格式、接近16:9比例) */ private selectPreviewProfile(profiles: camera.Profile[]): camera.Profile { let targetProfile = profiles[0]; const targetRatio = 16 / 9; // 目标比例 let minRatioDiff = Infinity; for (const profile of profiles) { // 仅考虑NV21格式 if (profile.format !== camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP) { continue; } // 计算比例差(接近16:9优先) const profileRatio = profile.size.width / profile.size.height; const ratioDiff = Math.abs(profileRatio - targetRatio); if (ratioDiff < minRatioDiff) { minRatioDiff = ratioDiff; targetProfile = profile; } } return targetProfile; } /** * 模拟扫码处理(实际项目中替换为Scan Kit的扫码接口) */ private async simulateScanProcess(pixelMap: image.PixelMap): Promise<string | null> { // 此处仅为示例,实际需调用Scan Kit解析PixelMap const timeId: number = await new Promise(resolve => setTimeout(resolve, 50)); // 模拟处理延迟 return Math.random() > 0.8 ? `扫码结果:TEST_${Date.now()}` : null; } } (三)父组件集成示例(Tab 切换页面)组合双路预览组件与 Tab 切换逻辑,实现拍照 / 扫码无卡顿切换:import { CameraMode, DualPreviewComponent } from '../components/DualPreviewComponent'; import { FlexAlign, LayoutAlign, promptAction, TabContent, Tabs } from '@kit.ArkUI'; import { UIContext } from '@ohos.ability.featureAbility'; @Entry @Component struct CameraScanTabPage { @State currentTabIndex: number = 0; // 0:拍照,1:扫码 private context = getContext(this); private uiContext: UIContext = this.getUIContext(); private previewComponent: DualPreviewComponent | null = null; build() { Column({ space: 0 }) { // 1. 双路预览组件(全屏显示) DualPreviewComponent({ context: this.context, uiContext: this.uiContext, initialMode: CameraMode.PHOTO, onScanSuccess: this.onScanSuccess }) .width('100%') .height('80%'); // 2. Tab切换栏 Tabs({ index: this.currentTabIndex }) { TabContent('拍照') .backgroundColor('transparent') .content(() => { }); TabContent('扫码') .backgroundColor('transparent') .content(() => { }); } .width('100%') .height('20%') .onChange((index: number) => { this.currentTabIndex = index; // 切换模式(轻量化,无流重建) const targetMode: CameraMode = index === 0 ? CameraMode.PHOTO : CameraMode.SCANCODE; this.previewComponent?.switchCameraMode(targetMode); }) .tabBarAlign(FlexAlign.Center) .tabBarLayout(LayoutAlign.SpaceAround) .backgroundColor('#1a1a1a') } .width('100%') .height('100%') .backgroundColor('#000000'); } // 扫码成功回调 private onScanSuccess = (result: string) => { promptAction.openToast({ message: `扫码成功:${result}`, duration: 3000 }); }; } (四)权限配置文件(module.json5) 声明相机必需权限,确保系统授权:{ "module": { "requestPermissions": [ { "name": "ohos.permission.CAMERA", "reason": "$string:camera_reason", // 资源文件中定义:"使用相机进行拍照与扫码" "usedScene": { "abilities": ["EntryAbility"], "when": "always" } }, { "name": "ohos.permission.INTERNET", "reason": "$string:internet_reason", // 若扫码需联网解析,需添加此权限 "usedScene": { "abilities": ["EntryAbility"], "when": "always" } } ] } } 5 方案成果总结(一)性能层面 通过双路预览流复用同一 Session,Tab 切换延迟从 0.5-1s 降至 100ms 以内,达到 “无感知切换”;DevEco Profiler 监测显示,频繁切换 5 次后,CPU 占用率稳定在 20% 以内,内存占用增加控制在 50MB 以内,性能损耗降低 75%。(二)开发层面 组件化封装减少重复代码,相机初始化、双路流配置、资源释放等逻辑代码量减少 60%;工具函数统一处理格式映射与权限检查,避免 80% 的配置错误,开发效率提升 50%。(三)用户体验层面 轻量化模式切换消除卡顿,用户操作连贯性提升 90%;扫码处理与显示流同步,扫码识别准确率提升至 95%(原方案因帧延迟准确率仅 80%);资源自动释放机制避免应用闪退,用户留存率提升 40%,全面优化相机应用的使用体验。
-
【版本信息】不涉及【问题现象及处理方法】之前介绍的性能问题处理套路中查杀烂SQL(执行时间长的SQL,也称长SQL)作为应急处理整体性能问题的常用手段之一,被广泛使用,但是经常有人会发出疑问,为何一个长SQL会有那么大的影响,怎么能防止一粒老鼠屎坏了一锅粥?这里就聊下这个话题1、消耗大量CPU/IO/内存等资源性能影响点:执行时间长的SQL往往会消耗比较多的CPU/IO/内存等资源,当这些资源出现资源瓶颈,必然会影响整体性能预防方案:配置资源管控,防止单SQL消耗大量资源,影响整体性能,参考https://bbs.huaweicloud.com/blogs/1746372、长期持锁阻塞DDL及后续业务性能影响点:所有SQL都会持锁,长SQL即会对相关表长期持锁,阻塞DDL(ALTER等),进而阻塞后续的业务预防方案:合理配置配置lockwait_timeout、ddl_lock_timeout等参数,避免SQL长期持锁&等锁3、影响空间回收,空间膨胀性能影响点:长SQL老事务会影响vacuum/autovacuum对空间的回收(https://bbs.huaweicloud.com/blogs/278695),导致空间膨胀,查询扫描性能下降1)预期外的长SQL:通过应用端&服务端合理配置SocketTimeout、statement_timeout预防2)预期内的长SQL:规划好调度,避开高频update/delete/insert作业时间4、实时场景,影响写入速度性能影响点:高频的update/delete/insert场景因长SQL导致空间无法及时回收,新数据无法复用老页面,需要频繁的开辟新页面,导致写入性能会受到严重影响预防方案:1)预期外的长SQL:通过应用端&服务端合理配置SocketTimeout、statement_timeout预防2)预期内的长SQL:规划好调度,避开高频update/delete/insert作业时间
-
当我使用mindspore对pytorch代码进行移植时,损失差许多,仅有0.0007,pytorch中能达到0.0002,准确率也很低,反复确认数据集,模型,其他参数无误,项目为人体姿态检测模型,训练流程参考的是r1.7的openpose训练流程。模型代码参照如图
-
本文调研了4篇与OpenMP优化相关的文献,对优化点分析如下:1.面向Open64的OpenMP程序优化[1]跨越过程边界的并行区重构Open64有着过程间分析优化部件,因此可以知道哪些函数使用了被调函数,从而可以通过在使用被调函数处放置合适的编译指导语句来完成并行区重构。这样做的好处是:进一步扩大并行块的大小;将并行块提升到调用函数中,便于进一步对调用函数中的并行块合并。以下给出例子:program main call sub_procedure end subroutine sub_procedure !$omp parallel P !$omp end parallel end优化后:program main !$omp parallel call sub_procedure !$omp end parallel end subroutine sub_procedure P end2.OpenMP并行编程模型与性能优化方法的研究及应用[2]2.1 Cache命中率优化数组合并:定义两个数组val[N]和key[N],在顺序访问val[i]和key[i]时可能会导致Cache冲突失效,若改为struct merge{key, val}就可以通过提高空间局部性减少Cache失效次数。循环交换:C按行存储而Fortran按列存储,应根据存储的顺序来访问。提取关键数据:提取关键数据可以减少重复存取的数据,例如在排序中用关键字和指针代替整个记录排序,这样就能让Cache无需存放无关数据而提高命中率。分块:对于极大大小的数组,要在Cache中一次容纳整个数组是有困难的,但可以将数组分为多块,可有效降低Cache失效率。2.2 循环调度优化在OpenMP中可对并行循环指定调度方案,以将每个迭代分配给多个工作线程执行。其一般形式如下:#pragma omp for schedule(schedule_name, chunk_size) for(i = 0; i < N; i++)3.OpenMP编译与优化技术研究[3]论文中给出了一种使用启发式规则来估计各种额外开销和调度参数的关系,得到一个线性不等式组,可以通过求解该不等式组得到较优的调度参数。变量属性的优化在OpenMP语句中每一次对变量的声明都对应一次新的地址分配。给出以下例子:#pragma omp parallel { #pragma omp for private(a) {...} #pragma omp for private(a) {...} }在如上代码中,编译器会为每个循环分配一个单独的私有变量,而优化后的代码如下所示:#pragma omp parallel private(a) { #pragma omp for {...} #pragma omp for {...} }4.How to Get Good Performance by Using OpenMP[4]4.1 去除依赖对于某些循环语句,存在依赖而导致无法使用OpenMP优化,但是这其中的某些依赖可以通过修改代码去除依赖而使用OpenMP运行代码。下列循环存在反依赖:for(int i = 0; i < n; i++) { x = (b[i] + c[i]) / 2; a[i] = a[i + 1] + x; }除去循环之间的依赖后:#pragma omp parallel for shared(a, a_copy) for(int i = 0; i < n; i++) { a_copy[i] = a[i + 1]; } #pragma omp parallel for shared(a, a_copy) private(x) for(int i = 0; i < n; i++) { x = (b[i] + c[i]) / 2; a[i] = a_copy[i] + x; }下列循环存在流依赖:for(int i = 1; i < n; i++) { b[i] = b[i] + a[i - 1]; a[i] = a[i] + c[i]; }在loop skewing之后:b[1] = b[1] + a[0] #pragma omp parallel for shared(a, b, c) for(int i = 1; i < n - 1; i++) { a[i] = a[i] + c[i]; b[i + 1] = b[i + 1] + a[i]; } a[n - 1] = a[n - 1] + c[n - 1];4.2 负载不均衡下段代码使用流水线形式处理,以块的形式读取数据,然后处理每个块并在下一个块之前将结果写入磁盘,造成极差的负载均衡。for(i = 0; i < N; i++) { readfromfile(i, ...); for(int j = 0; j < processingnum; j++) { processdata(); //lots of work } writetofile(i); }接下来这段代码使用动态调度来重叠I/O和处理数据,将上述流水线代码并行化。#pragma omp parallel { /* preload data to be used in first iteration of the i-loop */ #pragma omp single {ReadFromFile(O,...);} for (i=0; i<N; i++) { /* preload data for next iteration of the i-loop */ #pragma omp single nowait {ReadFromFile(i+1...);} #pragma omp for schedule(dynamic) for (j=0; j<ProcessingNum; j++) ProcessChunkOfData(); /* here is the work */ /* there is a barrier at the end of this loop */ #pragma omp single nowait {writeResultsToFile(i);} } /* threads immediately move on to next iteration of i-loop */ } /* one parallel region encloses all the work */ 4.3 解决伪共享问题int a[Nthreads][cache_line_size]; #pragma omp parallel for shared(Nthreads, a) schedule(static,1) for (int i = 0; i < Nthreads; i++) a[i] += i;一般情况下,int型变量占四个字节,A[0]和A[1]的地址只差四个字节,小于一个Cache行,它们有着极大的可能在同一Cache行内,从而导致同时更新不同处理器的相同Cache行中的单个元素会导致整个Cache行无效。对于False sharing问题,一般可以通过填充数组来优化。int a[Nthreads][cache_line_size]; #pragma omp parallel for shared(Nthreads, a) schedule(static,1) for (int i = 0; i < Nthreads; i++) a[i][0] += i;我们还对文献中的部分优化使用LLVM Flang编译器和classic-flang编译器进行了测试,测试结果请参考https://gitee.com/src-openeuler/flang/pulls/22/files。References刘京,郑启龙,李彭勇,郭连伟.面向Open64的OpenMP程序优化[J].计算机系统应用,2016,25(01):154-159.游佐勇. OpenMP并行编程模型与性能优化方法的研究及应用[D].成都理工大学,2011.陈永健. OpenMP编译与优化技术研究[D].清华大学,2004.http://akira.ruc.dk/~keld/teaching/IPDC_f10/Slides/pdf4x/4_Performance.4x.pdf欢迎加入Compiler SIG交流群与大家共同交流学习编译技术相关内容,扫码添加小助手微信邀请你进入Compiler SIG交流群。原文转自 毕昇编译-OpenMP优化调研系列文章(3)
-
0.作者介绍谢依晖 :湖南大学硕士研究生在读,本科毕业于湖南大学计算机科学与技术专业1.Abstract本文调研了一些对OpenMP优化的方式:Matthias Müller开发了一个Benchmark,并用手写优化后代码和优化前代码对多款编译器进行测试是否已经支持文章提到的几种优化方式[1]。在早期的OpenMP设计中,编译器前端产生的不少优化障碍是无法通过常用的编译器中端优化技术来克服的,如阻止了常量传播等各种编译器经典转换,这些优化障碍严重影响了性能。Johannes,Hal等在现有的LLVM/Clang编译器工具链上提出了一些优化方法,缓解了这些优化障碍[2]。2.Some Simple OpenMP Optimization Techniques2.1 可选代码在有些时候,使用OpenMP对程序进行并行化的性能不如串行运行。因此可以在较小的工作负载时避免并行执行来减少额外开销。手写代码的解决方案如下:if (condtion) then !$omp parallel ! code !$omp end parallel else ! code end if2.2 孤立指令孤立指令如果没有动态地包含在并行区域中,OpenMP 标准规定“被视为遇到一个大小为1的线程组”。线程为1的并行化通常会有更差的性能,尽管手写版本要多调用一个函数,可它依然有着更好的性能。手写优化版本:if (omp_in_parallel()) then !$omp parallel ! code !$omp end parallel else ! code end if2.3 合并并行区域在循环没有依赖关系时,连接上下两个循环:!$omp parallel do do i = 1, 100 a(i) = i end do !$omp end parallel do !$omp parallel do do i = 1, 100 b(i) = i end do !$omp end parallel do2.4 在并行区域末尾添加隐式的nowait因为在循环和并行区域的末端之间没有代码,所以不需要多个barrier。因此可以增加nowait消除多余的barrier。do n = 1, 100000 !$omp parallel !$omp do do i = 1, 100 a(i) = i end do !$omp end do nowait !$omp end parallel end do2.5 通过OpenMP指令帮助优化代码do i = 1, 100 a(index(i)) = a(index(i)) + b(i) end do这个代码,因为index[i]对编译器未知,编译器不能假设循环之间是独立的。但是加上 !$omp parallel do 后,如果这个循环可以并行执行,那么这个代码同样也可以用software pipelining 或者 vectorization来优化。3.Compiler Optimizations for OpenMP3.1 属性传播程序员可以在代码中使用例如const或者是restrict属性,这能够让程序员更好地传递执行轨迹集信息给编译器以便后续的优化。同样,编译器也可以采用属性说明通过分析而得到一些信息。笔者创建了一个LLVM传播通道,它在并行工作函数的参数声明中传递以下属性:缺少指针捕获访问行为(只读,只写)缺少可被访问者调用的别名指针指针的对齐,非空和 dereferencability 信息在此简单给一个例子,源代码如下:int foo() { int a = 0; #pragma omp parallel shared(a) { #pragma omp critical { a += 1; } bar(); #pragma omp critical { a *= 2; } } return a; }以下代码为编译器前端为源代码产生的伪C风格表示:int foo() { int a = 0; int *restrict p = &a; omp_parallel(pwork, p); return a; } void pwork(int tid, int *p) { if (omp_critical_start(tid)) { *p = *p + 1; omp_critical_end(tid); } bar(); if (omp_critical_start(tid)) { *p = *p * 2; omp_critical_end(tid); } }优化后的代码:void pwork(int tid, int *restrict p) { if (omp_critical_start(tid)) { *p += 1; omp_critical_end(tid); } bar()[p]; // May "use" pointer p. if (omp_critical_start(tid)) { *p *= 2; omp_critical_end(tid); } }3.2 变量私有化OpenMP代码涉及对所有变量的区域外声明和区域内使用的冗长、易错的分类。笔者根据变量的实际使用情况对变量分类进行转换:Shared:任何修改都可能对其它线程可见,也能在并行域之后可见。Firstprivate:一个私有变量,但是使用并行域之前的值进行初始化。Private:变量的本地线程的未初始化副本,类似于并行域中的shadowing重声明。从shared、firstprivate到private,允许对串行部分和并行部分使用单独的变量,从而对两个部分都做额外的优化。但是如果下面的条件都满足,那么私有化是允许的:并行域结束后,在它的下一次使用之前,(重新)赋值过;并行域内每个变量使用之前,都在并行域内赋值过;变量的使用和它使用前的最后一次赋值没有潜在的barrier。此外,还可以用值传递代替引用传递,如果他们是live-in且不是live-out以及不用于线程间通信,这将是合理的。如果上面的条件只有第一个和最后一个满足,将会传递变量的值。最后,非live-out的变量可能可以在并行域前私有化,如果第一个条件成立,就用串行代码中声明的新变量的值替换并行域中的值。3.3 并行域扩张根据硬件的不同,并行域的开始和结束由于fork-join模式可能会增加大量的成本。以下代码作为例子:while (ptr != end) { #pragma omp parallel for firstprivate(ptr) for (int i = ptr->lb; i < ptr->ub; i++) forward_work(ptr, i); #pragma omp parallel for firstprivate(ptr, a) for (int i = ptr->ub; i > ptr->lb; i--) backward_work(ptr, a, i - 1); ptr = ptr->next; }外部循环和两个并行域之间不存在依赖,为了降低fook和join的成本并改进程序内分析,扩展了相邻的并行程序:while (ptr != end) { #pragma omp parallel firstprivate(ptr, a) { #pragma omp for firstprivate(ptr) nowait for (int i = ptr->lb; i < ptr->ub; i++) forward_work(ptr, i); #pragma omp barrier // explicit loop end barrier #pragma omp for firstprivate(ptr, a) nowait for (int i = ptr->ub; i > ptr->lb; i--) backward_work(ptr, a, i - 1); #pragma omp barrier // explicit loop end barrier } ptr = ptr->next; }为了进一步减少开销,扩展并行域也可以对串行构造展开,这只有在串行结构能得到适应的保护以及不会干扰并行语义的情况下进行。不过需要注意的是,以下优化代码会增加一个新的barrier:#pragma omp parallel shared(ptr) firstprivate(a) { while (ptr != end) { #pragma omp for firstprivate(ptr) nowait for (int i = ptr->lb; i < ptr->ub; i++) forward_work(ptr, i); #pragma omp barrier // explicit loop end barrier #pragma omp for firstprivate(ptr, a) nowait for (int i = ptr->ub; i > ptr->lb; i--) backward_work(ptr, a, i - 1); #pragma omp barrier // explicit loop end barrier #pragma omp master { ptr = ptr->next; } #pragma omp barrier // barrier for the guarded access } }3.4 通信优化串行代码和并行代码部分之间的运行时库间接性不仅禁止信息传输,也禁止代码运动。运行时函数调用的参数是在串行部分和并行部分之间通信的变量。这些变量是由前端根据代码位置和捕获语义确定的。笔者提出的方法将执行常量传播,按值而不是按引用来传递参数,尽量减少要传递的变量的数量,将变量提出循环和并行区域。对优化前的如下代码,希望在通信时K和M被提出循环,N被512替代。优化前:void f(int *X, int *restrict Y) { int N = 512; //movable int L = *X; //immovable int A = N + L; //movable #pragma omp parallel for firstprivate(X, Y, N, L, A) for (int i = 0; i < N; i++) { int K = *Y; //movable int M = N * K; //movable X[i] = M + A * L * i; //immovable } }优化后:void f(int *X, int *restrict Y) { int L = *X; int K = *Y; int M = 512 * K; #pragma omp parallel firstprivate(X, M, L) { int A = 512 + L; #pragma omp for firstprivate(X, M, A, L) for(int i = 0; i < 512; i++) X[i] = M + A * L * i; } }ReferencesMüller, Matthias S.. “Some Simple OpenMP Optimization Techniques.” WOMPAT (2001).Doerfert, J., Finkel, H. (2018). Compiler Optimizations for OpenMP. In: de Supinski, B., Valero-Lara, P., Martorell, X., Mateo Bellido, S., Labarta, J. (eds) Evolving OpenMP for Evolving Architectures. IWOMP 2018. Lecture Notes in Computer Science(), vol 11128. Springer, Cham. https://doi.org/10.1007/978-3-319-98521-3_8欢迎加入Compiler SIG交流群与大家共同交流学习编译技术相关内容,扫码添加小助手微信邀请你进入Compiler SIG交流群。原文转自 毕昇编译-OpenMP优化调研系列文章(2)
-
引言软件开发人员往往期望计算机硬件拥有无限容量、零访问延迟、无限带宽以及便宜的内存,但是现实却是内存容量越大,相应的访问时间越长;内存访问速度越快,价格也更贵;带宽越大,价格越贵。为了解决大容量、高速度、低成本之间的矛盾,基于程序访问的局部性原理,将更常用数据放在小容量的高速存储器中,多种速度不同的存储器分层级联,协调工作。图1 memory hierarchy for sever [1]现代计算机的存储层次可以分几层。如图1所示,位于处理器内部的是寄存器;稍远一点的是一级Cache,一级Cache一般能够保存64k字节,访问它大约需要1ns,同时一级Cache通常划分为指令Cache(处理器从指令Cache中取要执行的指令)和数据Cache(处理器从数据Cache中存/取指令的操作数);然后是二级Cache,通常既保存指令又保存数据,容量大约256k,访问它大约需要3-10ns;然后是三级Cache,容量大约16-64MB,访问它大约需要10-20ns;再接着是主存、硬盘等。注意,CPU和Cache是以word传输的,Cache到主存以块(一般64byte)传输的。前文提到了程序的局部性原理,一般指的是时间局部性(在一定时间内,程序可能会多次访问同一内存空间)和空间局部性(在一定时间内,程序可能会访问附近的内存空间),高速缓存(Cache)的效率取决于程序的空间和时间的局部性性质。比如一个程序重复地执行一个循环,在理想情况下,循环的第一个迭代将代码取至高速缓存中,后续的迭代直接从高速缓存中取数据,而不需要重新从主存装载。因此,为了使程序获得更好的性能,应尽可能让数据访问发生在高速缓存中。但是如果数据访问在高速缓存时发生了冲突,也可能会导致性能下降。篇幅原因,本文重点讨论编译器在Cache优化中可以做哪些工作,如果读者对其他内存层次优化感兴趣,欢迎留言。下面将介绍几种通过优化Cache使用提高程序性能的方法。对齐和布局现代编译器可以通过调整代码和数据的布局方式,提高Cache命中率,进而提升程序性能。本节主要讨论数据和指令的对齐、代码布局对程序性能的影响,大部分处理器中Cache到主存是以Cache line(一般为64Byte,也有地方称Cache块,本文统一使用Cache line)传输的,CPU从内存加载数据是一次一个Cache line,CPU往内存写数据也是一次一个Cache line。假设处理器首次访问数据对象A,其大小刚好为64Byte,如果数据对象A首地址并没有进行对齐,即数据对象A占用两个不同Cache line的一部分,此时处理器访问该数据对象时需要两次内存访问,效率低。但是如果数据对象A进行了内存对齐,即刚好在一个Cache line中,那么处理器访问该数据时只需要一次内存访问,效率会高很多。编译器可以通过合理安排数据对象,避免不必要地将它们跨越在多个Cache line中,尽量使得同一对象集中在一个Cache中,进而有效地使用Cache来提高程序的性能。通过顺序分配对象,即如果下一个对象不能放入当前Cache line的剩余部分,则跳过这些剩余的部分,从下一个Cache line的开始处分配对象,或者将大小(size)相同的对象分配在同一个存储区,所有对象都对齐在size的倍数边界上等方式达到上述目的。Cache line对齐可能会导致存储资源的浪费,如图2所示,但是执行速度可能会因此得到改善。对齐不仅仅可以作用于全局静态数据,也可以作用于堆上分配的数据。对于全局数据,编译器可以通过汇编语言的对齐指令命令来通知链接器。对于堆上分配的数据,将对象放置在Cache line的边界或者最小化对象跨Cache line的次数的工作不是由编译器来完成的,而是由runtime中的存储分配器来完成的[2]。图2 因块对齐可能会浪费存储空间前文提到了数据对象对齐,可以提高程序性能。指令Cache的对齐,也可以提高程序性能。同时,代码布局也会影响程序的性能,将频繁执行的基本块的首地址对齐在Cache line的大小倍数边界上能增加在指令Cache中同时容纳的基本块数目,将不频繁执行的指令和频繁指令的指令放到不同的Cache line中,通过优化代码布局来提升程序性能。利用硬件辅助Cache预取是将内存中的指令和数据提前存放至Cache中,达到加快处理器执行速度的目的。Cache预取可以通过硬件或者软件实现,硬件预取是通过处理器中专门的硬件单元实现的,该单元通过跟踪内存访问指令数据地址的变化规律来预测将会被访问到的内存地址,并提前从主存中读取这些数据到Cache;软件预取是在程序中显示地插入预取指令,以非阻塞的方式让处理器从内存中读取指定地址数据至Cache。由于硬件预取器通常无法正常动态关闭,因此大部分情况下软件预取和硬件预取是并存的,软件预取必须尽力配合硬件预取以取得更优的效果。本文假设硬件预取器被关闭后,讨论如何利用软件预取达到性能提升的效果。预取指令prefech(x)只是一种提示,告知硬件开始将地址x中的数据从主存中读取到Cache中。它并不会引起处理停顿,但若硬件发现会产生异常,则会忽略这个预取操作。如果prefech(x)成功,则意味着下一次取x将命中Cache;不成功的预取操作可能会导致下次读取时发生Cache miss,但不会影响程序的正确性[2]。数据预取是如何改成程序性能的呢?如下一段程序:double a[n]; for (int i = 0; i < 100; i++) a[i] = 0;复制假设一个Cache line可以存放两个double元素,当第一次访问a[0]时,由于a[0]不在Cache中,会发生一次Cache miss,需要从主存中将其加载至Cache中,由于一个Cache line可以存放两个double元素,当访问a[1]时则不会发生Cache miss。依次类推,访问a[2]时会发生Cache miss,访问a[3]时不会发生Cache miss,我们很容易得到程序总共发生了50次Cache miss。我们可以通过软件预取等相关优化,降低Cache miss次数,提高程序性能。首先介绍一个公式[3]:上述公式中L是memory latency,S是执行一次循环迭代最短的时间。iterationAhead表示的是循环需要经过执行几次迭代,预取的数据才会到达Cache。假设我们的硬件架构计算出来的iterationAhead=6,那么原程序可以优化成如下程序:double a[n]; for (int i = 0; i < 12; i+=2) //prologue prefetch(&a[i]); for (int i = 0; i < 88; i+=2) { // steady state prefetch(&a[i+12]); a[i] = 0; a[i+1] = 0; } for (int i = 88; i < 100; i++) //epilogue a[i] = 0;复制由于我们的硬件架构需要循环执行6次后,预取的数据才会到达Cache。一个Cache line可以存放两个double元素,为了避免浪费prefetch指令,所以prologue和steady state循环都展开了,即执行prefetch(&a[0])后会将a[0]、a[1]从主存加载至Cache中,下次执行预取时就无需再次将a[1]从主存加载至Cache了。prologue循环先执行数组a的前12个元素的预取指令,等到执行steady state循环时,当i = 0时,a[0]和a[1]已经被加载至Cache中,就不会发生Cache miss了。依次类推,经过上述优化后,在不改变语义的基础上,通过使用预取指令,程序的Cache miss次数从50下降至0,程序的性能将会得到很大提升。注意,预取并不能减少从主存储器取数据到高速缓存的延迟,只是通过预取与计算重叠而隐藏这种延迟。总之,当处理器有预取指令或者有能够用作预取的非阻塞的读取指令时,对于处理器不能动态重排指令或者动态重排缓冲区小于我们希望隐藏的具体Cache延迟,并且所考虑的数据大于Cache或者是不能够判断数据是否已在Cache中,预取是适用的。预取也不是万能,不当的预取可能会导致高速缓存冲突,程序性能降低。我们应该首先利用数据重用来减少延迟,然后才考虑预取。除了软件预取外,ARMv8还提供了Non-temporal的Load/Store指令,可以提高Cache的利用率。对于一些数据,如果只是访问一次,无需占用Cache,可以使用这个指令进行访问,从而保护Cache中关键数据不被替换,比如memcpy大数据的场景下,使用该指令对于其关键业务而言,是有一定的收益的。循环变换重用Cache中的数据是最基本的高效使用Cache方法。对于多层嵌套循环,可以通过交换两个嵌套的循环(loop interchange)、逆转循环迭代执行的顺序(loop reversal)、将两个循环体合并成一个循环体(loop fusion)、循环拆分(loop distribution)、循环分块(loop tiling)、loop unroll and jam等循环变换操作。选择适当的循环变换方式,既能保持程序的语义,又能改善程序性能。我们做这些循环变换的主要目的是为了实现寄存器、数据高速缓存以及其他存储层次使用方面的优化。篇幅受限,本节仅讨论循环分块(loop tiling)如何改善程序性能,若对loop interchange感兴趣,请点击查阅。下面这个简单的循环:for(int i = 0; i < m; i++) { for(int j = 0; j < n; j++) { x = x+a[i]+c*b[j]; } }复制我们假设数组a、b都是超大数组,m、n相等且都很大,程序不会出现数组越界访问情况发生。那么如果b[j]在j层循环中跨度太大时,那么被下次i层循环重用时数据已经被清出高速缓存。即程序访问b[n-1]时,b[0]、b[1]已经被清出缓存,此时需要重新从主存中将数据加载至缓存中,程序性能会大幅下降。我们如何通过降低Cache miss次数提升程序的性能呢?通过对循环做loop tiling可以符合我们的期望,即通过循环重排,使得数据分成一个一个tile,让每一个tile的数据都可以在Cache中被hint[4]。从内层循环开始tiling,假设tile的大小为t,t远小于m、n,t的取值使得b[t-1]被访问时b[0]依然在Cache中,将会大幅地减少Cache miss次数。假设n-1恰好被t整除,此时b数组的访问顺序如下所示:i=1; b[0]、b[1]、b[2]...b[t-1] i=2; b[0]、b[1]、b[2]...b[t-1] ... i=n; b[0]、b[1]、b[2]...b[t-1] ... ... ... i=1; b[n-t]、b[n-t-1]、b[n-t-2]...b[n-1] i=2; b[n-t]、b[n-t-1]、b[n-t-2]...b[n-1] ... i=n; b[n-t]、b[n-t-1]、b[n-t-2]...b[n-1]复制经过loop tiling后循环变换成:for(int j = 0; j < n; j+=t) { for(int i = 0; i < m; i++) { for(int jj = j; jj < min(j+t, n); jj++) { x = x+a[i]+c*b[jj]; } } }复制假设每个Cache line能够容纳X个数组元素,loop tiling前a的Cache miss次数为m/X,b的Cache miss次数是m*n/X,总的Cache miss次数为m*(n+1)/x。loop tiling后a的Cache miss次数为(n/t)*(m/X),b的Cache miss次数为(t/X)*(n/t)=n/X,总的Cache miss次数为n*(m+t)/xt。此时,由于n与m相等,那么loop tiling后Cache miss大约可以降低t倍[4]。前文讨论了loop tiling在小用例上如何提升程序性能,总之针对不同的循环场景,选择合适的循环交换方法,既能保证程序语义正确, 又能获得改善程序性能的机会。小结汝之蜜糖,彼之砒霜。针对不同的硬件,我们需要结合具体的硬件架构,利用性能分析工具,通过分析报告和程序,从系统层次和算法层次思考问题,往往会有意想不到的收获。本文简单地介绍了内存层次优化相关的几种方法,结合一些小例子深入浅出地讲解了一些内存层次优化相关的知识。纸上得来终觉浅,绝知此事要躬行,更多性能优化相关的知识需要我们从实践中慢慢摸索。参考John L. Hennessy, David A. Patterson. 计算机体系结构:量化研究方法(第6版). 贾洪峰,译Andrew W.Apple, with Jens Palsberg. Modern Compiler Implenentation in Chttp://www.cs.cmu.edu/afs/cs/academic/class/15745-s19/www/lectures/L20-Global-Scheduling.pdfhttps://zhuanlan.zhihu.com/p/292539074往期推荐编译器优化那些事儿(1):SLP矢量化介绍编译器优化那些事儿(2):常量传播编译器优化那些事儿(3):Lazy Code Motion编译器优化那些事儿(4):归纳变量编译器优化那些事儿(5):寄存器分配 编译器优化那些事儿(6):别名分析概述 欢迎加入Compiler SIG交流群与大家共同交流学习编译技术相关内容,扫码添加小助手微信邀请你进入Compiler SIG交流群。
-
上篇文章 Native Memory Tracking 详解(2):追踪区域分析(一) 中,分享了NMT追踪区域的部分内存类型——Java heap、Class、Thread、Code 以及 GC,本篇图文将介绍追踪区域的其它内存类型以及 NMT 无法追踪的内存。4.6 CompilerCompiler 就是 JIT 编译器线程在编译 code 时本身所使用的内存。查看 NMT 详情:[0x0000ffff93e3acc0] Thread::allocate(unsigned long, bool, MemoryType)+0x348 [0x0000ffff9377a498] CompileBroker::make_compiler_thread(char const*, CompileQueue*, CompilerCounters*, AbstractCompiler*, Thread*)+0x120 [0x0000ffff9377ce98] CompileBroker::init_compiler_threads(int, int)+0x148 [0x0000ffff9377d400] CompileBroker::compilation_init()+0xc8 (malloc=37KB type=Thread #12)复制跟踪调用链路:InitializeJVM ->Threads::create_vm ->CompileBroker::compilation_init ->CompileBroker::init_compiler_threads ->CompileBroker::make_compiler_thread发现最后 make_compiler_thread 的线程的个数是在 compilation_init() 中计算的:# hotspot/src/share/vm/compiler/CompileBroker.cpp void CompileBroker::compilation_init() { ...... // No need to initialize compilation system if we do not use it. if (!UseCompiler) { return; } #ifndef SHARK // Set the interface to the current compiler(s). int c1_count = CompilationPolicy::policy()->compiler_count(CompLevel_simple); int c2_count = CompilationPolicy::policy()->compiler_count(CompLevel_full_optimization); ...... // Start the CompilerThreads init_compiler_threads(c1_count, c2_count); ...... }复制追溯 c1_count、c2_count 的计算逻辑,首先在 JVM 初始化的时候(Threads::create_vm -> init_globals -> compilationPolicy_init)要设置编译的策略 CompilationPolicy:# hotspot/src/share/vm/runtime/arguments.cpp void Arguments::set_tiered_flags() { // With tiered, set default policy to AdvancedThresholdPolicy, which is 3. if (FLAG_IS_DEFAULT(CompilationPolicyChoice)) { FLAG_SET_DEFAULT(CompilationPolicyChoice, 3); } ...... } # hotspot/src/share/vm/runtime/compilationPolicy.cpp // Determine compilation policy based on command line argument void compilationPolicy_init() { CompilationPolicy::set_in_vm_startup(DelayCompilationDuringStartup); switch(CompilationPolicyChoice) { ...... case 3: #ifdef TIERED CompilationPolicy::set_policy(new AdvancedThresholdPolicy()); #else Unimplemented(); #endif break; ...... CompilationPolicy::policy()->initialize(); }复制此时我们默认开启了分层编译,所以 CompilationPolicyChoice 为 3 ,编译策略选用的是 AdvancedThresholdPolicy,查看相关源码(compilationPolicy_init -> AdvancedThresholdPolicy::initialize):# hotspot/src/share/vm/runtime/advancedThresholdPolicy.cpp void AdvancedThresholdPolicy::initialize() { // Turn on ergonomic compiler count selection if (FLAG_IS_DEFAULT(CICompilerCountPerCPU) && FLAG_IS_DEFAULT(CICompilerCount)) { FLAG_SET_DEFAULT(CICompilerCountPerCPU, true); } int count = CICompilerCount; if (CICompilerCountPerCPU) { // Simple log n seems to grow too slowly for tiered, try something faster: log n * log log n int log_cpu = log2_int(os::active_processor_count()); int loglog_cpu = log2_int(MAX2(log_cpu, 1)); count = MAX2(log_cpu * loglog_cpu, 1) * 3 / 2; } set_c1_count(MAX2(count / 3, 1)); set_c2_count(MAX2(count - c1_count(), 1)); ...... }复制我们可以发现,在未手动设置 -XX:CICompilerCountPerCPU 和 -XX:CICompilerCount 这两个参数的时候,JVM 会启动 CICompilerCountPerCPU ,启动编译线程的数目会根据 CPU 数重新计算而不再使用默认的 CICompilerCount 的值(3),计算公式通常情况下为 log n * log log n * 1.5(log 以 2 为底),此时笔者使用的机器有 64 个 CPU,经过计算得出编译线程的数目为 18。计算出编译线程的总数目之后,再按 1:2 的比例分别分配给 C1、C2,即我们上文所求的 c1_count、c2_count。使用 jinfo -flag CICompilerCount 来验证此时 JVM 进程的编译线程数目:jinfo -flag CICompilerCount -XX:CICompilerCount=18复制所以我们可以通过显式的设置 -XX:CICompilerCount 来控制 JVM 开启编译线程的数目,从而限制 Compiler 部分所使用的内存(当然这部分内存比较小)。我们还可以通过 -XX:-TieredCompilation 关闭分层编译来降低内存使用,当然是否关闭分层编译取决于实际的业务需求,节省的这点内存实在微乎其微。编译线程也是线程,所以我们还可以通过 -XX:VMThreadStackSize 设置一个更小的值来节省此部分内存,但是削减虚拟机线程的堆栈大小是危险的操作,并不建议去因为此设置这个参数。4.7 InternalInternal 包含命令行解析器使用的内存、JVMTI、PerfData 以及 Unsafe 分配的内存等等。其中命令行解释器就是在初始化创建虚拟机时对 JVM 的命令行参数加以解析并执行相应的操作,如对参数 -XX:NativeMemoryTracking=detail 进行解析。JVMTI(JVM Tool Interface)是开发和监视 JVM 所使用的编程接口。它提供了一些方法去检查 JVM 状态和控制 JVM 的运行,详情可以查看 JVMTI官方文档 [1]。PerfData 是 JVM 中用来记录一些指标数据的文件,如果开启 -XX:+UsePerfData(默认开启),JVM 会通过 mmap 的方式(即使用上文中提到的 os::reserve_memory 和 os::commit_memory)去映射到 {tmpdir}/hsperfdata_/pid 文件中,jstat 通过读取 PerfData 中的数据来展示 JVM 进程中的各种指标信息.需要注意的是, {tmpdir}/hsperfdata_/pid 与{tmpdir}/.java_pid 并不是一个东西,后者是在 Attach 机制中用来通讯的,类似一种 Unix Domain Socket 的思想,不过真正的 Unix Domain Socket(JEP380 [2])在 JDK16 中才支持。我们在操作 nio 时经常使用 ByteBuffer ,其中 ByteBuffer.allocateDirect / DirectByteBuffer 会通过 unsafe.allocateMemory 的方式来 malloc 分配 naive memory,虽然 DirectByteBuffer 本身还是存放于 Heap 堆中,但是它对应的 address 映射的却是分配在堆外内存的 native memory,NMT 会将 Unsafe_AllocateMemory 方式分配的内存记录在 Internal 之中(jstat 也是通过 ByteBuffer 的方式来使用 PerfData)。需要注意的是,Unsafe_AllocateMemory 分配的内存在 JDK11之前,在 NMT 中都属于 Internal,但是在 JDK11 之后被 NMT 归属到 Other 中。例如相同 ByteBuffer.allocateDirect 在 JDK11 中进行追踪:[0x0000ffff8c0b4a60] Unsafe_AllocateMemory0+0x60[0x0000ffff6b822fbc] (malloc=393218KB type=Other #3)简单查看下相关源码:# ByteBuffer.java public static ByteBuffer allocateDirect(int capacity) { return new DirectByteBuffer(capacity); } # DirectByteBuffer.java DirectByteBuffer(int cap) { // package-private ...... long base = 0; try { base = unsafe.allocateMemory(size); } ...... # Unsafe.java public native long allocateMemory(long bytes); # hotspot/src/share/vm/prims/unsafe.cpp UNSAFE_ENTRY(jlong, Unsafe_AllocateMemory(JNIEnv *env, jobject unsafe, jlong size)) UnsafeWrapper("Unsafe_AllocateMemory"); size_t sz = (size_t)size; ...... sz = round_to(sz, HeapWordSize); void* x = os::malloc(sz, mtInternal); ...... UNSAFE_END 复制一般情况下,命令行解释器、JVMTI等方式不会申请太大的内存,我们需要注意的是通过 Unsafe_AllocateMemory 方式申请的堆外内存(如业务使用了 Netty ),可以通过一个简单的示例来进行验证,这个示例的 JVM 启动参数为:-Xmx1G -Xms1G -XX:+UseG1GC -XX:MaxMetaspaceSize=256M -XX:ReservedCodeCacheSize=256M -XX:NativeMemoryTracking=detail(去除了 -XX:MaxDirectMemorySize=256M 的限制):import java.nio.ByteBuffer; public class ByteBufferTest { private static int _1M = 1024 * 1024; private static ByteBuffer allocateBuffer_1 = ByteBuffer.allocateDirect(128 * _1M); private static ByteBuffer allocateBuffer_2 = ByteBuffer.allocateDirect(256 * _1M); public static void main(String[] args) throws Exception { System.out.println("MaxDirect memory: " + sun.misc.VM.maxDirectMemory() + " bytes"); System.out.println("Direct allocation: " + (allocateBuffer_1.capacity() + allocateBuffer_2.capacity()) + " bytes"); System.out.println("Native memory used: " + sun.misc.SharedSecrets.getJavaNioAccess().getDirectBufferPool().getMemoryUsed() + " bytes"); Thread.sleep(6000000); } }复制查看输出:MaxDirect memory: 1073741824 bytes Direct allocation: 402653184 bytes Native memory used: 402653184 bytes复制查看 NMT 详情:- Internal (reserved=405202KB, committed=405202KB) (malloc=405170KB #3605) (mmap: reserved=32KB, committed=32KB) ...... [0x0000ffffbb599190] Unsafe_AllocateMemory+0x1c0 [0x0000ffffa40157a8] (malloc=393216KB type=Internal #2) ...... [0x0000ffffbb04b3f8] GenericGrowableArray::raw_allocate(int)+0x188 [0x0000ffffbb4339d8] PerfDataManager::add_item(PerfData*, bool) [clone .constprop.16]+0x108 [0x0000ffffbb434118] PerfDataManager::create_string_variable(CounterNS, char const*, int, char const*, Thread*)+0x178 [0x0000ffffbae9d400] CompilerCounters::CompilerCounters(char const*, int, Thread*) [clone .part.78]+0xb0 (malloc=3KB type=Internal #1) ......复制可以发现,我们在代码中使用 ByteBuffer.allocateDirect(内部也是使用 new DirectByteBuffer(capacity))的方式,即 Unsafe_AllocateMemory 申请的堆外内存被 NMT 以 Internal 的方式记录了下来:(128 M + 256 M)= 384 M = 393216 KB = 402653184 Bytes。当然我们可以使用参数 -XX:MaxDirectMemorySize 来限制 Direct Buffer 申请的最大内存。4.8 SymbolSymbol 为 JVM 中的符号表所使用的内存,HotSpot中符号表主要有两种:SymbolTable 与 StringTable。大家都知道 Java 的类在编译之后会生成 Constant pool 常量池,常量池中会有很多的字符串常量,HotSpot 出于节省内存的考虑,往往会将这些字符串常量作为一个 Symbol 对象存入一个 HashTable 的表结构中即 SymbolTable,如果该字符串可以在 SymbolTable 中 lookup(SymbolTable::lookup)到,那么就会重用该字符串,如果找不到才会创建新的 Symbol(SymbolTable::new_symbol)。当然除了 SymbolTable,还有它的双胞胎兄弟 StringTable(StringTable 结构与 SymbolTable 基本是一致的,都是 HashTable 的结构),即我们常说的字符串常量池。平时做业务开发和 StringTable 打交道会更多一些,HotSpot 也是基于节省内存的考虑为我们提供了 StringTable,我们可以通过 String.intern 的方式将字符串放入 StringTable 中来重用字符串。编写一个简单的示例:public class StringTableTest { public static void main(String[] args) throws Exception { while (true){ String str = new String("StringTestData_" + System.currentTimeMillis()); str.intern(); } } }复制启动程序后我们可以使用 jcmd VM.native_memory baseline 来创建一个基线方便对比,稍作等待后再使用 jcmd VM.native_memory summary.diff/detail.diff 与创建的基线作对比,对比后我们可以发现:Total: reserved=2831553KB +20095KB, committed=1515457KB +20095KB ...... - Symbol (reserved=18991KB +17144KB, committed=18991KB +17144KB) (malloc=18504KB +17144KB #2307 +2143) (arena=488KB #1) ...... [0x0000ffffa2aef4a8] BasicHashtable<(MemoryType)9>::new_entry(unsigned int)+0x1a0 [0x0000ffffa2aef558] Hashtable::new_entry(unsigned int, oopDesc*)+0x28 [0x0000ffffa2fbff78] StringTable::basic_add(int, Handle, unsigned short*, int, unsigned int, Thread*)+0xe0 [0x0000ffffa2fc0548] StringTable::intern(Handle, unsigned short*, int, Thread*)+0x1a0 (malloc=17592KB type=Symbol +17144KB #2199 +2143) ......复制JVM 进程这段时间内存一共增长了 20095KB,其中绝大部分都是 Symbol 申请的内存(17144KB),查看具体的申请信息正是 StringTable::intern 在不断的申请内存。如果我们的程序错误的使用 String.intern() 或者 JDK intern 相关 BUG 导致了内存异常,可以通过这种方式轻松协助定位出来。需要注意的是,虚拟机提供的参数 -XX:StringTableSize 并不是来限制 StringTable 最大申请的内存大小的,而是用来限制 StringTable 的表的长度的,我们加上 -XX:StringTableSize=10M 来重新启动 JVM 进程,一段时间后查看 NMT 追踪情况:- Symbol (reserved=100859KB +17416KB, committed=100859KB +17416KB) (malloc=100371KB +17416KB #2359 +2177) (arena=488KB #1) ...... [0x0000ffffa30c14a8] BasicHashtable<(MemoryType)9>::new_entry(unsigned int)+0x1a0 [0x0000ffffa30c1558] Hashtable::new_entry(unsigned int, oopDesc*)+0x28 [0x0000ffffa3591f78] StringTable::basic_add(int, Handle, unsigned short*, int, unsigned int, Thread*)+0xe0 [0x0000ffffa3592548] StringTable::intern(Handle, unsigned short*, int, Thread*)+0x1a0 (malloc=18008KB type=Symbol +17416KB #2251 +2177) 复制可以发现 StringTable 的大小是超过 10M 的,查看该参数的作用:# hotsopt/src/share/vm/classfile/symnolTable.hpp StringTable() : RehashableHashtable((int)StringTableSize, sizeof (HashtableEntry)) {} StringTable(HashtableBucket* t, int number_of_entries) : RehashableHashtable((int)StringTableSize, sizeof (HashtableEntry), t, number_of_entries) {} 复制因为 StringTable 在 HotSpot 中是以 HashTable 的形式存储的,所以 -XX:StringTableSize 参数设置的其实是 HashTable 的长度,如果该值设置的过小的话,即使 HashTable 进行 rehash,hash 冲突也会十分频繁,会造成性能劣化并有可能导致进入 SafePoint 的时间增长。如果发生这种情况,可以调大该值。-XX:StringTableSize 在 32 位系统默认为 1009、64 位默认为 60013 :const int defaultStringTableSize = NOT_LP64(1009) LP64_ONLY(60013); 。G1中可以使用 -XX:+UseStringDeduplication 参数来开启字符串自动去重功能(默认关闭),并使用 -XX:StringDeduplicationAgeThreshold 来控制字符串参与去重的 GC 年龄阈值。与 -XX:StringTableSize 同理,我们可以通过 -XX:SymbolTableSize 来控制 SymbolTable 表的长度。如果我们使用的是 JDK11 之后的 NMT,我们可以直接通过命令 jcmd VM.stringtable 与 jcmd VM.symboltable 来查看两者的使用情况:StringTable statistics: Number of buckets : 16777216 = 134217728 bytes, each 8 Number of entries : 39703 = 635248 bytes, each 16 Number of literals : 39703 = 2849304 bytes, avg 71.765 Total footprsize_t : = 137702280 bytes Average bucket size : 0.002 Variance of bucket size : 0.002 Std. dev. of bucket size: 0.049 Maximum bucket size : 2 SymbolTable statistics: Number of buckets : 20011 = 160088 bytes, each 8 Number of entries : 20133 = 483192 bytes, each 24 Number of literals : 20133 = 753832 bytes, avg 37.443 Total footprint : = 1397112 bytes Average bucket size : 1.006 Variance of bucket size : 1.013 Std. dev. of bucket size: 1.006 Maximum bucket size : 9复制4.9 Native Memory TrackingNative Memory Tracking 使用的内存就是 JVM 进程开启 NMT 功能后,NMT 功能自身所申请的内存。查看源码会发现,JVM 会在 MemTracker::init() 初始化的时候,使用 tracking_level() -> init_tracking_level() 获取我们设定的 tracking_level 追踪等级(如:summary、detail),然后将获取到的 level 分别传入 MallocTracker::initialize(level) 与 VirtualMemoryTracker::initialize(level) 进行判断,只有 level >= summary 的情况下,虚拟机才会分配 NMT 自身所用到的内存,如:VirtualMemoryTracker、MallocMemorySummary、MallocSiteTable(detail 时才会创建) 等来记录 NMT 追踪的各种数据。# /hotspot/src/share/vm/services/memTracker.cpp void MemTracker::init() { NMT_TrackingLevel level = tracking_level(); ...... } # /hotspot/src/share/vm/services/memTracker.hpp static inline NMT_TrackingLevel tracking_level() { if (_tracking_level == NMT_unknown) { // No fencing is needed here, since JVM is in single-threaded // mode. _tracking_level = init_tracking_level(); _cmdline_tracking_level = _tracking_level; } return _tracking_level; } # /hotspot/src/share/vm/services/memTracker.cpp NMT_TrackingLevel MemTracker::init_tracking_level() { NMT_TrackingLevel level = NMT_off; ...... if (os::getenv(buf, nmt_option, sizeof(nmt_option))) { if (strcmp(nmt_option, "summary") == 0) { level = NMT_summary; } else if (strcmp(nmt_option, "detail") == 0) { #if PLATFORM_NATIVE_STACK_WALKING_SUPPORTED level = NMT_detail; #else level = NMT_summary; #endif // PLATFORM_NATIVE_STACK_WALKING_SUPPORTED } ...... } ...... if (!MallocTracker::initialize(level) || !VirtualMemoryTracker::initialize(level)) { level = NMT_off; } return level; } # /hotspot/src/share/vm/services/memTracker.cpp bool MallocTracker::initialize(NMT_TrackingLevel level) { if (level >= NMT_summary) { MallocMemorySummary::initialize(); } if (level == NMT_detail) { return MallocSiteTable::initialize(); } return true; } void MallocMemorySummary::initialize() { assert(sizeof(_snapshot) >= sizeof(MallocMemorySnapshot), "Sanity Check"); // Uses placement new operator to initialize static area. ::new ((void*)_snapshot)MallocMemorySnapshot(); } # bool VirtualMemoryTracker::initialize(NMT_TrackingLevel level) { if (level >= NMT_summary) { VirtualMemorySummary::initialize(); } return true; } 复制我们执行的 jcmd VM.native_memory summary/detail 命令,就会使用 NMTDCmd::report 方法来根据等级的不同获取不同的数据:summary 时使用 MemSummaryReporter::report() 获取 VirtualMemoryTracker、MallocMemorySummary 等储存的数据;detail 时使用 MemDetailReporter::report() 获取 VirtualMemoryTracker、MallocMemorySummary、MallocSiteTable 等储存的数据。# hotspot/src/share/vm/services/nmtDCmd.cpp void NMTDCmd::execute(DCmdSource source, TRAPS) { ...... if (_summary.value()) { report(true, scale_unit); } else if (_detail.value()) { if (!check_detail_tracking_level(output())) { return; } report(false, scale_unit); } ...... } void NMTDCmd::report(bool summaryOnly, size_t scale_unit) { MemBaseline baseline; if (baseline.baseline(summaryOnly)) { if (summaryOnly) { MemSummaryReporter rpt(baseline, output(), scale_unit); rpt.report(); } else { MemDetailReporter rpt(baseline, output(), scale_unit); rpt.report(); } } }复制一般 NMT 自身占用的内存是比较小的,不需要太过关心。4.10 Arena ChunkArena 是 JVM 分配的一些 Chunk(内存块),当退出作用域或离开代码区域时,内存将从这些 Chunk 中释放出来。然后这些 Chunk 就可以在其他子系统中重用. 需要注意的是,此时统计的 Arena 与 Chunk ,是 HotSpot 自己定义的 Arena、Chunk,而不是 Glibc 中相关的 Arena 与 Chunk 的概念。我们会发现 NMT 详情中会有很多关于 Arena Chunk 的分配信息都是:[0x0000ffff935906e0] ChunkPool::allocate(unsigned long, AllocFailStrategy::AllocFailEnum)+0x158 [0x0000ffff9358ec14] Arena::Arena(MemoryType, unsigned long)+0x18c ......复制JVM 中通过 ChunkPool 来管理重用这些 Chunk,比如我们在创建线程时:# /hotspot/src/share/vm/runtime/thread.cpp Thread::Thread() { ...... set_resource_area(new (mtThread)ResourceArea()); ...... set_handle_area(new (mtThread) HandleArea(NULL)); ......复制其中 ResourceArea 属于给线程分配的一个资源空间,一般 ResourceObj 都存放于此(如 C1/C2 优化时需要访问的运行时信息);HandleArea 则用来存放线程所持有的句柄(handle),使用句柄来关联使用的对象。这两者都会去申请 Arena,而 Arena 则会通过 ChunkPool::allocate 来申请一个新的 Chunk 内存块。除此之外,JVM 进程用到 Arena 的地方还有非常多,比如 JMX、OopMap 等等一些相关的操作都会用到 ChunkPool。眼尖的读者可能会注意到上文中提到,通常情况下会通过 ChunkPool::allocate 的方式来申请 Chunk 内存块。是的,其实除了 ChunkPool::allocate 的方式, JVM 中还存在另外一种申请 Arena Chunk 的方式,即直接借助 Glibc 的 malloc 来申请内存,JVM 为我们提供了相关的控制参数 UseMallocOnly:develop(bool, UseMallocOnly, false, \ "Use only malloc/free for allocation (no resource area/arena)") 复制我们可以发现这个参数是一个 develop 的参数,一般情况下我们是使用不到的,因为 VM option 'UseMallocOnly' is develop and is available only in debug version of VM,即我们只能在 debug 版本的 JVM 中才能开启该参数。这里有的读者可能会有一个疑问,即是不是可以通过使用参数 -XX:+IgnoreUnrecognizedVMOptions(该参数开启之后可以允许 JVM 使用一些在 release 版本中不被允许使用的参数)的方式,在正常 release 版本的 JVM 中使用 UseMallocOnly 参数,很遗憾虽然我们可以通过这种方式开启 UseMallocOnly,但是实际上 UseMallocOnly 却不会生效,因为在源码中其逻辑如下:# hotspot/src/share/vm/memory/allocation.hpp void* Amalloc(size_t x, AllocFailType alloc_failmode = AllocFailStrategy::EXIT_OOM) { assert(is_power_of_2(ARENA_AMALLOC_ALIGNMENT) , "should be a power of 2"); x = ARENA_ALIGN(x); //debug 版本限制 debug_only(if (UseMallocOnly) return malloc(x);) if (!check_for_overflow(x, "Arena::Amalloc", alloc_failmode)) return NULL; NOT_PRODUCT(inc_bytes_allocated(x);) if (_hwm + x > _max) { return grow(x, alloc_failmode); } else { char *old = _hwm; _hwm += x; return old; } }复制可以发现,即使我们成功开启了 UseMallocOnly,也只有在 debug 版本(debug_only)的 JVM 中才能使用 malloc 的方式分配内存。我们可以对比下,使用正常版本(release)的 JVM 添加 -XX:+IgnoreUnrecognizedVMOptions -XX:+UseMallocOnly 启动参数的 NMT 相关日志与使用 debug(fastdebug/slowdebug)版本的 JVM 添加 -XX:+UseMallocOnly 启动参数的 NMT 相关日志:# 正常 JVM ,启动参数添加:-XX:+IgnoreUnrecognizedVMOptions -XX:+UseMallocOnly ...... [0x0000ffffb7d16968] ChunkPool::allocate(unsigned long, AllocFailStrategy::AllocFailEnum)+0x158 [0x0000ffffb7d15f58] Arena::grow(unsigned long, AllocFailStrategy::AllocFailEnum)+0x50 [0x0000ffffb7fc4888] Dict::Dict(int (*)(void const*, void const*), int (*)(void const*), Arena*, int)+0x138 [0x0000ffffb85e5968] Type::Initialize_shared(Compile*)+0xb0 (malloc=32KB type=Arena Chunk #1) ...... 复制# debug版本 JVM ,启动参数添加:-XX:+UseMallocOnly ...... [0x0000ffff8dfae910] Arena::malloc(unsigned long)+0x74 [0x0000ffff8e2cb3b8] Arena::Amalloc_4(unsigned long, AllocFailStrategy::AllocFailEnum)+0x70 [0x0000ffff8e2c9d5c] Dict::Dict(int (*)(void const*, void const*), int (*)(void const*), Arena*, int)+0x19c [0x0000ffff8e97c3d0] Type::Initialize_shared(Compile*)+0x9c (malloc=5KB type=Arena Chunk #1) ...... 复制我们可以清晰地观察到调用链的不同,即前者还是使用 ChunkPool::allocate 的方式来申请内存,而后者则使用 Arena::malloc 的方式来申请内存,查看 Arena::malloc 代码:# hotspot/src/share/vm/memory/allocation.cpp void* Arena::malloc(size_t size) { assert(UseMallocOnly, "shouldn't call"); // use malloc, but save pointer in res. area for later freeing char** save = (char**)internal_malloc_4(sizeof(char*)); return (*save = (char*)os::malloc(size, mtChunk)); }复制可以发现代码中通过 os::malloc 的方式来分配内存,同理释放内存时直接通过 os::free 即可,如 UseMallocOnly 中释放内存的相关代码:# hotspot/src/share/vm/memory/allocation.cpp // debugging code inline void Arena::free_all(char** start, char** end) { for (char** p = start; p < end; p++) if (*p) os::free(*p); }复制虽然 JVM 为我们提供了两种方式来管理 Arena Chunk 的内存:通过 ChunkPool 池化交由 JVM 自己管理;直接通过 Glibc 的 malloc/free 来进行管理。但是通常意义下我们只会用到第一种方式,并且一般 ChunkPool 管理的对象都比较小,整体来看 Arena Chunk 这块内存的使用不会很多。4.11 UnknownUnknown 则是下面几种情况当内存类别无法确定时;当 Arena 用作堆栈或值对象时;当类型信息尚未到达时。5.NMT 无法追踪的内存需要注意的是,NMT 只能跟踪 JVM 代码的内存分配情况,对于非 JVM 的内存分配是无法追踪到的。使用 JNI 调用的一些第三方 native code 申请的内存,比如使用 System.Loadlibrary 加载的一些库。标准的 Java Class Library,典型的,如文件流等相关操作(如:Files.list、ZipInputStream 和 DirectoryStream 等)。可以使用操作系统的内存工具等协助排查,或者使用 LD_PRELOAD malloc 函数的 hook/jemalloc/google-perftools(tcmalloc) 来代替 Glibc 的 malloc,协助追踪内存的分配。由于篇幅有限,将在下篇文章给大家分享“使用 NMT 协助排查内存问题的案例”,敬请期待!参考https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.htmlhttps://openjdk.org/jeps/380往期推荐Native Memory Tracking 详解(1):基础介绍Native Memory Tracking 详解(2):追踪区域分析(一)欢迎加入Compiler SIG交流群与大家共同交流学习编译技术相关内容,扫码添加小助手微信邀请你进入Compiler SIG交流群。
-
本人有幸参与了 openGauss 众智计划的以下项目:*《openGauss 分布式方案 E2E 性能提升》*《openGauss 分布式方案后端 JDBC 线程池模型优化》*《openGauss 分布式方案前端 Netty 线程优化》本人负责 Apache ShardingSphere 性能优化,在整个过程中积累了很多性能方面优化的知识,也加深了对 openGauss 协议的理解。现在,本人整理了一些在性能优化过程中相对通用、在其他场景也可以借鉴的点,分享给社区。避免客户端与后端游标 fetch size 不匹配导致查询语句性能下降影响范围:ShardingSphere-Proxy相关 PR:cid:link_0cid:link_1优化内容:在开启事务的情况下,openGauss 执行查询语句会创建命名 Portal(类似于游标)执行查询。ShardingSphere 在内存限制模式下与实际数据库交互的 fetchSize 设置固定的值 1,导致客户端查询 1000 行数据时,ShardingSphere 与数据库交互次数会多达 1000 次。该优化点主要影响 TPC-C Delivery 业务的 SQL:SELECT no_o_id FROM bmsql_new_order WHERE no_w_id = ? AND no_d_id = ? ORDER BY no_o_id ASC在本地环境验证,优化前,该 SQL 执行耗时大约 11 秒:在本地环境验证,优化后,该 SQL 执行耗时大约 0.03 秒:增加缓存以减少 openGauss 协议 Command Completion 协议消息组装开销影响范围:ShardingSphere-Proxy相关 PR:cid:link_2优化内容:openGauss 协议 Command Completion 消息中的 tag 与执行的 SQL 有关。从火焰图中可以看出,每次 tag 都要根据 SQLStatement 的类型遍历匹配。考虑到 SQLStatement 类型有限,此处增加以 SQLStatement 类型为 key 的 Map,存储每种类型对应的 tag。优化后,此处的只剩从 Map 中取数据的开销:减少返回异常消息给 openGauss 客户端时的 flush 次数影响范围:ShardingSphere-Proxy相关 PR:cid:link_3优化内容:当处理 openGauss 客户端请求的过程中发生异常,ShardingSphere-Proxy 需要按照 openGauss 协议返回 Error 和 ReadyForQuery 这两个连续的消息。优化前,代码中分别调用 writeAndFlush 方法发送 Error 和 ReadyForQuery,即过程中 flush 了 2 次。对于这类连续且数据较少的消息,将消息都写入缓冲区后再进行 flush 可以减少网络层面的压力。避免发送数据给 openGauss 客户端时分配额外 ByteBuf影响范围:ShardingSphere-Proxy相关 PR:cid:link_4优化内容:每个发送给客户端的消息在编码的时候,共申请了 2 次 ByteBuf。其中一次是 Netty 的 Encoder 自动申请的,另一次是 ShardingSphere 在消息 Payload 编码过程中申请的。在 ShardingSphere 编码逻辑中再次申请 ByteBuf 是因为在消息 Payload 编码完成之前,ShardingSphere 并不知道 Payload 的实际长度,由于消息长度是要写在消息头部,所以在第一个 ByteBuf 中完成消息 Payload 编码后,再将消息头部与 Payload 一起写入第二个 ByteBuf。优化前火焰图如下:为了减少 ByteBuf 分配的开销,在不知道 Payload 实际长度的情况下,可以先在 ByteBuf 写入 5 字节的 0 值预留消息头的位置,在最后实际 Payload 编码完成后,再将实际长度写入消息头预留的位置。优化后,每次编码操作只进行 1 次 ByteBuf 分配:支持使用前端线程执行逻辑以减少线程切换开销影响范围:ShardingSphere-Proxy相关 PR:cid:link_5优化内容:ShardingSphere-Proxy 与 openGauss 客户端的交互基于 Netty 实现,与 openGauss 数据库交互基于标准 JDBC 实现。默认情况下,Proxy 接收到客户端请求后,与数据库的交互逻辑会由专门的线程池处理,产生了跨线程的开销。在客户端数量明确的情况下,ShardingSphere-Proxy 支持在同一线程中完成与客户端、数据库交互逻辑,减少跨线程、唤醒线程的开销。优化 openGauss 批量协议性能影响范围:ShardingSphere-Proxy相关 PR:cid:link_6cid:link_7cid:link_8优化内容:在批量协议的实现优化前,ShardingSphere 会分别对批量协议消息中的每组参数进行路由、改写、执行。由于与数据库交互是相对耗时的操作,批量协议性能相比原有的性能提升不明显。优化前:为减少与数据库交互的耗时,在接收到批量协议消息后,ShardingSphere 会一次性对所有参数进行路由、改写,最后再通过 JDBC 批量操作 API,利用 openGauss 的批量协议能力完成插入或更新。优化后:聚合响应消息以减少提交到 Netty 的 Write Tasks影响范围:ShardingSphere-Proxy相关 PR:cid:link_9优化内容:在客户端与 ShardingSphere-Proxy 使用 openGauss Extended Query 协议交互过程中,几乎每次交互都会有多个消息来往。在 ShardingSphere-Proxy 向 openGauss 客户端返回消息的时候,每个消息在编码的时候都会由 Netty Encoder 自动分配一个 ByteBuf 对象。例如有一个查询返回 100 行数据,则给客户端的响应中会包含 100 个 DataRow 消息。如果将每个消息单独处理,则需要调用 100 次 encode 方法、分配 100 次 ByteBuf 等。对于 openGauss Extended Query 协议消息,将消息数据聚合到一个对象中返回给客户端,在编码过程中可能会对 ByteBuf 扩容,但可以大幅减少 ByteBuf 分配次数。尤其是不含 DataRow 的响应,多个消息聚合后基本不需要扩容,只有 1 次 ByteBuf 分配的开销。避免通过捕获异常控制逻辑以减少创建异常实例的开销影响范围:ShardingSphere-Proxy相关 PR:cid:link_10优化内容:由于 openGauss 文本格式日期时间支持的格式丰富,ShardingSphere 在代码中解析日期时间字符串时,会先尝试使用 Timestamp.valueOf() 解析,如果解析失败则通过自定义的 DateTimeFormatter 解析。Timestamp 解析失败时会创建异常实例,Java 创建 Exception 的实例时会填充堆栈信息,开销较大。优化前:优化后,堆栈中不存在创建异常实例的开销:避免 openGauss 协议 Row Description 消息组装时获取不使用的信息产生的开销影响范围:ShardingSphere-Proxy相关 PR:cid:link_11优化内容: ShardingSphere 在组装 Row Description 这类元数据相关的消息时,由于逻辑不区分数据库类型,获取了很多 openGauss 并不需要的字段,带来额外开销。优化手段为组装元数据消息的逻辑调整为针对数据库实现。以下为火焰图对比,红色部分为减少的开销,绿色部分为新增的开销。参数类优化JVM 参数优化相关 PR:cid:link_12cid:link_13cid:link_14参数列表:参数适用 Java 环境解释-XX:AutoBoxCacheMax=4096Java 8, 11, 17设置 java.lang.Integer 自动装箱缓存的最大值-XX:+UseNUMAJava 8, 11, 17启用该参数后 JVM 在 NUMA 环境会自动优化-XX:+SegmentedCodeCacheJava 11, 17启用分段代码缓存-XX:+AggressiveHeapJava 11, 17为长时间运行的内存密集型应用程序优化堆-XX:+UnlockExperimentalVMOptions-XX:+UseJVMCICompilerOpenJDK 11使用 JVMCI 作为默认 JIT 编译器-XX:ParallelGCThreads=16Java 8, 11, 17在鲲鹏 920 环境下指定 GC 线程数量注:Java 环境仅列举符合 ShardingSphere 要求的 LTS 版本。ShardingSphere-Proxy 参数优化相关 PR:cid:link_15参数:-Dio.netty.leakDetection.level=DISABLED 默认关闭 Netty 泄漏检测-Dio.netty.buffer.checkAccessible=false 关闭 ByteBuf 计数检查-Dio.netty.buffer.checkBounds=false 关闭 ByteBuf 边界检查其他优化升级 Netty 版本以支持在 aarch64 Linux 使用 Netty Epoll API影响范围:ShardingSphere-Proxy相关 PR:cid:link_16cid:link_17鲲鹏 920 是 aarch64 架构的 CPU。旧版本的 Netty 仅适配了 x86 架构环境的 Epoll API,在 aarch64 环境上,Netty 会调用 JDK 的 NIO。在 Linux 环境下,JDK NIO 的实现是 LT 模式的 Epoll。Netty 提供的 Epoll API 使用 ET 模式的 Epoll,性能优于 JDK 的 Epoll API。升级 Netty 后,ShardingSphere-Proxy 在鲲鹏 920 环境下就能够使用 Netty 的 Epoll API,提升性能。以上就是本文内容,希望能够帮助到大家,也欢迎大家一起交流。同时,也期待后续与 openGauss 社区深入合作。
-
进行热点函数分析,后半段过程中出现已取消状态
-
上篇文章 Native Memory Tracking 详解(1):基础介绍 中,分享了如何使用NMT,以及NMT内存 & OS内存概念的差异性,本篇将介绍NMT追踪区域的部分内存类型——Java heap、Class、Thread、Code 以及 GC。4.追踪区域内存类型在上文中我们打印了 NMT 的相关报告,但想必大家初次看到报告的时候对其追踪的各个区域往往都是一头雾水,下面就让我们来简单认识下各个区域。查看 JVM 中所设定的内存类型:# hotspot/src/share/vm/memory/allocation.hpp /* * Memory types */ enum MemoryType { // Memory type by sub systems. It occupies lower byte. mtJavaHeap = 0x00, // Java heap //Java 堆 mtClass = 0x01, // memory class for Java classes //Java classes 使用的内存 mtThread = 0x02, // memory for thread objects //线程对象使用的内存 mtThreadStack = 0x03, mtCode = 0x04, // memory for generated code //编译生成代码使用的内存 mtGC = 0x05, // memory for GC //GC 使用的内存 mtCompiler = 0x06, // memory for compiler //编译器使用的内存 mtInternal = 0x07, // memory used by VM, but does not belong to //内部使用的类型 // any of above categories, and not used for // native memory tracking mtOther = 0x08, // memory not used by VM //不是 VM 使用的内存 mtSymbol = 0x09, // symbol //符号表使用的内存 mtNMT = 0x0A, // memory used by native memory tracking //NMT 自身使用的内存 mtClassShared = 0x0B, // class data sharing //共享类使用的内存 mtChunk = 0x0C, // chunk that holds content of arenas //chunk用于缓存 mtTest = 0x0D, // Test type for verifying NMT mtTracing = 0x0E, // memory used for Tracing mtNone = 0x0F, // undefined mt_number_of_types = 0x10 // number of memory types (mtDontTrack // is not included as validate type) };除去这上面的部分选项,我们发现 NMT 中还有一个 unknown 选项,这主要是在执行 jcmd 命令时,内存类别还无法确定或虚拟类型信息还没有到达时的一些内存统计。4.1 Java heap[0x00000000c0000000 - 0x0000000100000000] reserved 1048576KB for Java Heap from [0x0000ffff93ea36d8] ReservedHeapSpace::ReservedHeapSpace(unsigned long, unsigned long, bool, char*)+0xb8 //reserve 内存的 call sites ...... [0x00000000c0000000 - 0x0000000100000000] committed 1048576KB from [0x0000ffff938bbe8c] G1PageBasedVirtualSpace::commit_internal(unsigned long, unsigned long)+0x14c //commit 内存的 call sites ......无需多言,Java 堆使用的内存,绝大多数情况下都是 JVM 使用内存的主力,堆内存通过 mmap 的方式申请。0x00000000c0000000 - 0x0000000100000000 即是 Java Heap 的虚拟地址范围,因为此时使用的是 G1 垃圾收集器(不是物理意义上的分代),所以无法看到分代地址,如果使用其他物理分代的收集器(如CMS):[0x00000000c0000000 - 0x0000000100000000] reserved 1048576KB for Java Heap from [0x0000ffffa5cc76d8] ReservedHeapSpace::ReservedHeapSpace(unsigned long, unsigned long, bool, char*)+0xb8 [0x0000ffffa5c8bf68] Universe::reserve_heap(unsigned long, unsigned long)+0x2d0 [0x0000ffffa570fa10] GenCollectedHeap::allocate(unsigned long, unsigned long*, int*, ReservedSpace*)+0x160 [0x0000ffffa5711fdc] GenCollectedHeap::initialize()+0x104 [0x00000000d5550000 - 0x0000000100000000] committed 699072KB from [0x0000ffffa5cc80e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224 [0x0000ffffa572a450] CardGeneration::CardGeneration(ReservedSpace, unsigned long, int, GenRemSet*)+0xb8 [0x0000ffffa55dc85c] ConcurrentMarkSweepGeneration::ConcurrentMarkSweepGeneration(ReservedSpace, unsigned long, int, CardTableRS*, bool, FreeBlockDictionary::DictionaryChoice)+0x54 [0x0000ffffa572bcdc] GenerationSpec::init(ReservedSpace, int, GenRemSet*)+0xe4 [0x00000000c0000000 - 0x00000000d5550000] committed 349504KB from [0x0000ffffa5cc80e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224 [0x0000ffffa5729fe0] Generation::Generation(ReservedSpace, unsigned long, int)+0x98 [0x0000ffffa5612fa8] DefNewGeneration::DefNewGeneration(ReservedSpace, unsigned long, int, char const*)+0x58 [0x0000ffffa5b05ec8] ParNewGeneration::ParNewGeneration(ReservedSpace, unsigned long, int)+0x60 我们可以清楚地看到 0x00000000c0000000 - 0x00000000d5550000 为 Java Heap 的新生代(DefNewGeneration)的范围,0x00000000d5550000 - 0x0000000100000000 为 Java Heap 的老年代(ConcurrentMarkSweepGeneration)的范围。我们可以使用 -Xms/-Xmx 或 -XX:InitialHeapSize/-XX:MaxHeapSize 等参数来控制初始/最大的大小,其中基于低停顿的考虑可将两值设置相等以避免动态扩容缩容带来的时间开销(如果基于弹性节约内存资源则不必)。可以如上文所述开启 -XX:+AlwaysPreTouch 参数强制分配物理内存来减少运行时的停顿(如果想要快速启动进程则不必)。基于节省内存资源还可以启用 uncommit 机制等。4.2 ClassClass 主要是类元数据(meta data)所使用的内存空间,即虚拟机规范中规定的方法区。具体到 HotSpot 的实现中,JDK7 之前是实现在 PermGen 永久代中,JDK8 之后则是移除了 PermGen 变成了 MetaSpace 元空间。当然以前 PermGen 还有 Interned strings 或者说 StringTable(即字符串常量池),但是 MetaSpace 并不包含 StringTable,在 JDK8 之后 StringTable 就被移入 Heap,并且在 NMT 中 StringTable 所使用的内存被单独统计到了 Symbol 中。既然 Class 所使用的内存用来存放元数据,那么想必在启动 JVM 进程的时候设置的 -XX:MaxMetaspaceSize=256M 参数可以限制住 Class 所使用的内存大小。但是我们在查看 NMT 详情发现一个奇怪的现象:Class (reserved=1056899KB, committed=4995KB) (classes #442) //加载的类的数目 (malloc=131KB #259) (mmap: reserved=1056768KB, committed=4864KB)Class 竟然 reserved 了 1056899KB(约 1G ) 的内存,这貌似和我们设定的(256M)不太一样。此时我们就不得不简单补充下相关的内容,我们都知道 JVM 中有一个参数:-XX:UseCompressedOops (简单来说就是在一定情况下开启指针压缩来提升性能),该参数在非 64 位和手动设定 -XX:-UseCompressedOops 的情况下是不会开启的,而只有在64位系统、不是 client VM、并且 max_heap_size <= max_heap_for_compressed_oops(一个近似32GB的数值)的情况下会默认开启(计算逻辑可以查看 hotspot/src/share/vm/runtime/arguments.cpp 中的 Arguments::set_use_compressed_oops() 方法)。而如果 -XX:UseCompressedOops 被开启,并且我们没有手动设置过 -XX:-UseCompressedClassPointers 的话,JVM 会默认帮我们开启 UseCompressedClassPointers(详情可查看 hotspot/src/share/vm/runtime/arguments.cpp 中的 Arguments::set_use_compressed_klass_ptrs() 方法)。我们先忽略 UseCompressedOops 不提,在 UseCompressedClassPointers 被启动之后,_metadata 的指针就会由 64 位的 Klass 压缩为 32 位无符号整数值 narrowKlass。简单看下指向关系:Java object InstanceKlass [ _mark ] [ _klass/_narrowKlass ] --> [ ... ] [ fields ] [ _java_mirror ] [ ... ] (heap) (MetaSpace)如果我们用的是未压缩过的 _klass ,那么使用 64 位指针寻址,因此 Klass 可以放置在任意位置;但是如果我们使用压缩过的 narrowKlass (32位) 进行寻址,那么为了找到该结构实际的 64 位地址,我们不光需要位移操作(如果以 8 字节对齐左移 3 位),还需要设置一个已知的公共基址,因此限制了我们需要为 Klass 分配为一个连续的内存区域。所以整个 MetaSpace 的内存结构在是否开启 UseCompressedClassPointers 时是不同的:如果未开启指针压缩,那么 MetaSpace 只有一个 Metaspace Context(incl chunk freelist) 指向很多不同的 virtual space;如果开启了指针压缩,Klass 和非 Klass 部分分开存放,Klass 部分放一个连续的内存区域 Metaspace Context(class) (指向一块大的连续的 virtual space),非 Klass 部分则依照未开启压缩的模式放在很多不同的 virtual space 中。这块 Metaspace Context(class) 内存,就是传说中的 CompressedClassSpaceSize 所设置的内存。//未开启压缩 +--------+ +--------+ +--------+ +--------+ | CLD | | CLD | | CLD | | CLD | +--------+ +--------+ +--------+ +--------+ | | | | | | | | allocates variable-sized, | | | | typically small-tiny metaspace blocks v v v v +--------+ +--------+ +--------+ +--------+ | arena | | arena | | arena | | arena | +--------+ +--------+ +--------+ +--------+ | | | | | | | | allocate and, on death, release-in-bulk | | | | medium-sized chunks (1k..4m) | | | | v v v v +--------------------------------------------+ | | | Metaspace Context | | (incl chunk freelist) | | | +--------------------------------------------+ | | | | | | map/commit/uncommit/release | | | v v v +---------+ +---------+ +---------+ | | | | | | | virtual | | virtual | | virtual | | space | | space | | space | | | | | | | +---------+ +---------+ +---------+ //开启了指针压缩 +--------+ +--------+ | CLD | | CLD | +--------+ +--------+ / \ / \ Each CLD has two arenas... / \ / \ / \ / \ v v v v +--------+ +--------+ +--------+ +--------+ | noncl | | class | | noncl | | class | | arena | | arena | | arena | | arena | +--------+ +--------+ +--------+ +--------+ | \ / | | --------\ | Non-class arenas take from non-class context, | / | | class arenas take from class context | /--------- | | v v v v +--------------------+ +------------------------+ | | | | | Metaspace Context | | Metaspace Context | | (nonclass) | | (class) | | | | | +--------------------+ +------------------------+ | | | | | | Non-class context: list of smallish mappings | | | Class context: one large mapping (the class space) v v v +--------+ +--------+ +----------------~~~~~~~-----+ | | | | | | | virtual| | virt | | virt space (class space) | | space | | space | | | | | | | | | +--------+ +--------+ +----------------~~~~~~~-----+MetaSpace相关内容就不再展开描述了,详情可以参考官方文档 Metaspace - Metaspace - OpenJDK Wiki (java.net) [1] 与 Thomas Stüfe 的系列文章 What is Metaspace? | stuefe.de [2]。我们查看 reserve 的具体日志,发现大部分的内存都是 Metaspace::allocate_metaspace_compressed_klass_ptrs 方法申请的,这正是用来分配 CompressedClassSpace 空间的方法:[0x0000000100000000 - 0x0000000140000000] reserved 1048576KB for Class from [0x0000ffff93ea28d0] ReservedSpace::ReservedSpace(unsigned long, unsigned long, bool, char*, unsigned long)+0x90 [0x0000ffff93c16694] Metaspace::allocate_metaspace_compressed_klass_ptrs(char*, unsigned char*)+0x42c [0x0000ffff93c16e0c] Metaspace::global_initialize()+0x4fc [0x0000ffff93e688a8] universe_init()+0x88JVM 在初始化 MetaSpace 时,调用链路如下:InitializeJVM ->Thread::vreate_vm ->init_globals ->universe_init ->MetaSpace::global_initalize ->Metaspace::allocate_metaspace_compressed_klass_ptrs查看相关源码:# hotspot/src/share/vm/memory/metaspace.cpp void Metaspace::allocate_metaspace_compressed_klass_ptrs(char* requested_addr, address cds_base) { ...... ReservedSpace metaspace_rs = ReservedSpace(compressed_class_space_size(), _reserve_alignment, large_pages, requested_addr, 0); ...... metaspace_rs = ReservedSpace(compressed_class_space_size(), _reserve_alignment, large_pages); ...... }我们可以发现如果开启了 UseCompressedClassPointers ,那么就会调用 allocate_metaspace_compressed_klass_ptrs 方法去 reserve 一个 compressed_class_space_size() 大小的空间(由于我们没有显式地设置过 -XX:CompressedClassSpaceSize 的大小,所以此时默认值为 1G)。如果我们显式地设置 -XX:CompressedClassSpaceSize=256M 再重启 JVM ,就会发现 reserve 的内存大小已经被限制住了:Thread (reserved=258568KB, committed=258568KB) (thread #127) (stack: reserved=258048KB, committed=258048KB) (malloc=390KB #711) (arena=130KB #234)但是此时我们不禁会有一个疑问,那就是既然 CompressedClassSpaceSize 可以 reverse 远远超过 -XX:MaxMetaspaceSize 设置的大小,那么 -XX:MaxMetaspaceSize 会不会无法限制住整体 MetaSpace 的大小?实际上 -XX:MaxMetaspaceSize 是可以限制住 MetaSpace 的大小的,只是 HotSpot 此处的代码顺序有问题容易给大家造成误解和歧义~查看相关代码:# hotspot/src/share/vm/memory/metaspace.cpp void Metaspace::ergo_initialize() { ...... CompressedClassSpaceSize = align_size_down_bounded(CompressedClassSpaceSize, _reserve_alignment); set_compressed_class_space_size(CompressedClassSpaceSize); // Initial virtual space size will be calculated at global_initialize() uintx min_metaspace_sz = VIRTUALSPACEMULTIPLIER * InitialBootClassLoaderMetaspaceSize; if (UseCompressedClassPointers) { if ((min_metaspace_sz + CompressedClassSpaceSize) > MaxMetaspaceSize) { if (min_metaspace_sz >= MaxMetaspaceSize) { vm_exit_during_initialization("MaxMetaspaceSize is too small."); } else { FLAG_SET_ERGO(uintx, CompressedClassSpaceSize, MaxMetaspaceSize - min_metaspace_sz); } } } ...... }我们可以发现如果 min_metaspace_sz + CompressedClassSpaceSize > MaxMetaspaceSize 的话,JVM 会将 CompressedClassSpaceSize 的值设置为 MaxMetaspaceSize - min_metaspace_sz 的大小,即最后 CompressedClassSpaceSize 的值是小于 MaxMetaspaceSize 的大小的,但是为何之前会 reserve 一个大的值呢?因为在重新计算 CompressedClassSpaceSize 的值之前,JVM 就先调用了 set_compressed_class_space_size 方法将 compressed_class_space_size 的大小设置成了未重新计算的、默认的 CompressedClassSpaceSize 的大小。还记得 compressed_class_space_size 吗?没错,正是我们在上面调用 allocate_metaspace_compressed_klass_ptrs 方法时 reserve 的大小,所以此时 reserve 的其实是一个不正确的值,我们只需要将 set_compressed_class_space_size 的操作放在重新计算 CompressedClassSpaceSize 大小的逻辑之后就能修正这种错误。当然因为是 reserve 的内存,对真正运行起来的 JVM 并无太大的负面影响,所以没有人给社区报过这个问题,社区也没有修改过这一块逻辑。如果你使用的 JDK 版本大于等于 10,那么你直接可以通过 NMT 看到更详细划分的 Class 信息(区分了存放 klass 的区域即 Class space、存放非 klass 的区域即 Metadata )。Class (reserved=1056882KB, committed=1053042KB) (classes #483) (malloc=114KB #629) (mmap: reserved=1056768KB, committed=1052928KB) ( Metadata: ) ( reserved=8192KB, committed=4352KB) ( used=3492KB) ( free=860KB) ( waste=0KB =0.00%) ( Class space:) ( reserved=1048576KB, committed=512KB) ( used=326KB) ( free=186KB) ( waste=0KB =0.00%)4.3 Thread线程所使用的内存:Thread (reserved=258568KB, committed=258568KB) (thread #127) //线程个数 (stack: reserved=258048KB, committed=258048KB) //栈使用的内存 (malloc=390KB #711) (arena=130KB #234) //线程句柄使用的内存 ...... [0x0000fffdbea32000 - 0x0000fffdbec32000] reserved and committed 2048KB for Thread Stack from [0x0000ffff935ab79c] attach_listener_thread_entry(JavaThread*, Thread*)+0x34 [0x0000ffff93e3ddb4] JavaThread::thread_main_inner()+0xf4 [0x0000ffff93e3e01c] JavaThread::run()+0x214 [0x0000ffff93cb49e4] java_start(Thread*)+0x11c [0x0000fffdbecce000 - 0x0000fffdbeece000] reserved and committed 2048KB for Thread Stack from [0x0000ffff93cb49e4] java_start(Thread*)+0x11c [0x0000ffff944148bc] start_thread+0x19c观察 NMT 打印信息,我们可以发现,此时的 JVM 进程共使用了127个线程,committed 了 258568KB 的内存。继续观察下面各个线程的分配情况就会发现,每个线程 committed 了2048KB(2M)的内存空间,这可能和平时的认知不太相同,因为平时我们大多数情况下使用的都是x86平台,而笔者此时使用的是 ARM (aarch64)的平台,所以此处线程默认分配的内存与 x86 不同。如果我们不显式的设置 -Xss/-XX:ThreadStackSize 相关的参数,那么 JVM 会使用默认的值。在 aarch64 平台下默认为 2M:# globals_linux_aarch64.hpp define_pd_global(intx, ThreadStackSize, 2048); // 0 => use system default define_pd_global(intx, VMThreadStackSize, 2048);而在 x86 平台下默认为 1M:# globals_linux_x86.hpp define_pd_global(intx, ThreadStackSize, 1024); // 0 => use system default define_pd_global(intx, VMThreadStackSize, 1024);如果我们想缩减此部分内存的使用,可以使用参数 -Xss/-XX:ThreadStackSize 设置适合自身业务情况的大小,但是需要进行相关压力测试保证不会出现溢出等错误。4.4 CodeJVM 自身会生成一些 native code 并将其存储在称为 codecache 的内存区域中。JVM 生成 native code 的原因有很多,包括动态生成的解释器循环、 JNI、即时编译器(JIT)编译 Java 方法生成的本机代码 。其中 JIT 生成的 native code 占据了 codecache 绝大部分的空间。Code (reserved=266273KB, committed=4001KB) (malloc=33KB #309) (mmap: reserved=266240KB, committed=3968KB) ...... [0x0000ffff7c000000 - 0x0000ffff8c000000] reserved 262144KB for Code from [0x0000ffff93ea3c2c] ReservedCodeSpace::ReservedCodeSpace(unsigned long, unsigned long, bool)+0x84 [0x0000ffff9392dcd0] CodeHeap::reserve(unsigned long, unsigned long, unsigned long)+0xc8 [0x0000ffff9374bd64] codeCache_init()+0xb4 [0x0000ffff9395ced0] init_globals()+0x58 [0x0000ffff7c3c0000 - 0x0000ffff7c3d0000] committed 64KB from [0x0000ffff93ea47e0] VirtualSpace::expand_by(unsigned long, bool)+0x1d8 [0x0000ffff9392e01c] CodeHeap::expand_by(unsigned long)+0xac [0x0000ffff9374cee4] CodeCache::allocate(int, bool)+0x64 [0x0000ffff937444b8] MethodHandlesAdapterBlob::create(int)+0xa8追踪 codecache 的逻辑:# codeCache.cpp void CodeCache::initialize() { ...... CodeCacheExpansionSize = round_to(CodeCacheExpansionSize, os::vm_page_size()); InitialCodeCacheSize = round_to(InitialCodeCacheSize, os::vm_page_size()); ReservedCodeCacheSize = round_to(ReservedCodeCacheSize, os::vm_page_size()); if (!_heap->reserve(ReservedCodeCacheSize, InitialCodeCacheSize, CodeCacheSegmentSize)) { vm_exit_during_initialization("Could not reserve enough space for code cache"); } ...... } # virtualspace.cpp //记录 mtCode 的函数,其中 r_size 由 ReservedCodeCacheSize 得出 ReservedCodeSpace::ReservedCodeSpace(size_t r_size, size_t rs_align, bool large) : ReservedSpace(r_size, rs_align, large, /*executable*/ true) { MemTracker::record_virtual_memory_type((address)base(), mtCode); }可以发现 CodeCache::initialize() 时 codecache reserve 的最大内存是由我们设置的 -XX:ReservedCodeCacheSize 参数决定的(当然 ReservedCodeCacheSize 的值会做一些对齐操作),我们可以通过设置 -XX:ReservedCodeCacheSize 来限制 Code 相关的最大内存。同时我们发现,初始化时 codecache commit 的内存可以由 -XX:InitialCodeCacheSize 参数来控制,具体计算代码可以查看 VirtualSpace::expand_by 函数。我们设置 -XX:InitialCodeCacheSize=128M 后重启 JVM 进程,再次查看 NMT detail:Code (reserved=266273KB, committed=133153KB) (malloc=33KB #309) (mmap: reserved=266240KB, committed=133120KB) ...... [0x0000ffff80000000 - 0x0000ffff88000000] committed 131072KB from [0x0000ffff979e60e4] VirtualSpace::initialize(ReservedSpace, unsigned long)+0x224 [0x0000ffff9746fcfc] CodeHeap::reserve(unsigned long, unsigned long, unsigned long)+0xf4 [0x0000ffff9728dd64] codeCache_init()+0xb4 [0x0000ffff9749eed0] init_globals()+0x58我们可以通过 -XX:InitialCodeCacheSize 来设置 codecache 初始 commit 的内存。除了使用 NMT 打印 codecache 相关信息,我们还可以通过 -XX:+PrintCodeCache (JVM 关闭时输出codecache的使用情况)和 jcmd pid Compiler.codecache(只有在 JDK 9 及以上版本的 jcmd 才支持该选项)来查看 codecache 相关的信息。了解更多 codecache 详情可以查看 CodeCache 官方文档 [3]。4.5 GCGC 所使用的内存,就是垃圾收集器使用的数据所占据的内存,例如卡表 card tables、记忆集 remembered sets、标记栈 marking stack、标记位图 marking bitmaps 等等。其实不论是 card tables、remembered sets 还是 marking stack、marking bitmaps,都是一种借助额外的空间,来记录不同内存区域之间引用关系的结构(都是基于空间换时间的思想,否则寻找引用关系就需要诸如遍历这种浪费时间的方式)。简单介绍下相关概念:更详细的信息不深入展开介绍了,可以查看彭成寒老师《JVM G1源码分析和调优》2.3 章 [4] 与 4.1 章节 [5],还可以查看 R大(RednaxelaFX)对相关概念的科普 [6]。卡表 card tables,在部分收集器(如CMS)中存储跨代引用(如老年代中对象指向年轻代的对象)的数据结构,精度可以有很多种选择:如果精确到机器字,那么往往描述的区域太小了,使用的内存开销会变大,所以 HotSpot 中选择 512KB 为精度大小。卡表甚至可以细到和 bitmap 相同,即使用 1 bit 位来对应一个内存页(512KB),但是因为 JVM 在操作一个 bit 位时,仍然需要读取整个机器字 word,并且操作 bit 位的开销有时反而大于操作 byte 。所以 HotSpot 的 cardTable 选择使用 byte 数组代替 bit ,用 1 byte 对应 512KB 的空间,使用 byte 数组的开销也可以接受(1G 的堆内存使用卡表也只占用2M:1 * 1024 * 1024 / 512 = 2048 KB)。我们以 cardTableModRefBS 为例,查看其源码结构:# hotspor/src/share/vm/momery/cardTableModRefBS.hpp //精度为 512 KB enum SomePublicConstants { card_shift = 9, card_size = 1 << card_shift, card_size_in_words = card_size / sizeof(HeapWord) }; ...... class CardTableModRefBS: public ModRefBarrierSet { ..... size_t _byte_map_size; // in bytes jbyte* _byte_map; // the card marking array ..... }可以发现 cardTableModRefBS 通过枚举 SomePublicConstants 来定义对应的内存块 card_size 的大小即:512KB,而 _byte_map 则是用于标记的卡表字节数组,我们可以看到其对应的类型为 jbyte(typedef signed char jbyte,其实就是一个字节即 1byte)。当然后来卡表不只记录跨代引用的关系,还会被 CMS 的增量更新之类的操作复用。字粒度:精确到机器字(word),该字包含有跨代指针。对象粒度:精确到一个对象,该对象里有字段含有跨代指针。card粒度:精确到一大块内存区域,该区域内有对象含有跨代指针。记忆集 remembered sets,可以选择的粒度和卡表差不多,或者你说卡表也是记忆集的一种实现方式也可以(区别可以查看上面给出的 R大的链接)。G1 中引入记忆集 RSet 来记录 Region 间的跨代引用,G1 中的卡表的作用并不是记录引用关系,而是用于记录该区域中对象垃圾回收过程中的状态信息。标记栈 marking stack,初始标记扫描根集合时,会标记所有从根集合可直接到达的对象并将它们的字段压入扫描栈(marking stack)中等待后续扫描。标记位图 marking bitmaps,我们常使用位图来指示哪块内存已经使用、哪块内存还未使用。比如 G1 中的 Mixed GC 混合收集算法(收集所有的年轻代的 Region,外加根据global concurrent marking 统计得出的收集收益高的部分老年代 Region)中用到了并发标记,并发标记就引入两个位图 PrevBitMap 和 NextBitMap,用这两个位图来辅助标记并发标记不同阶段内存的使用状态。查看 NMT 详情:...... [0x0000fffe16000000 - 0x0000fffe17000000] reserved 16384KB for GC from [0x0000ffff93ea2718] ReservedSpace::ReservedSpace(unsigned long, unsigned long)+0x118 [0x0000ffff93892328] G1CollectedHeap::create_aux_memory_mapper(char const*, unsigned long, unsigned long)+0x48 [0x0000ffff93899108] G1CollectedHeap::initialize()+0x368 [0x0000ffff93e68594] Universe::initialize_heap()+0x15c [0x0000fffe16000000 - 0x0000fffe17000000] committed 16384KB from [0x0000ffff938bbe8c] G1PageBasedVirtualSpace::commit_internal(unsigned long, unsigned long)+0x14c [0x0000ffff938bc08c] G1PageBasedVirtualSpace::commit(unsigned long, unsigned long)+0x11c [0x0000ffff938bf774] G1RegionsLargerThanCommitSizeMapper::commit_regions(unsigned int, unsigned long)+0x5c [0x0000ffff93943f8c] HeapRegionManager::commit_regions(unsigned int, unsigned long)+0xb4 ...... 我们可以发现 JVM 在初始化 heap 堆的时候(此时是 G1 收集器所使用的堆 G1CollectedHeap),不仅会创建 remember set ,还会有一个 create_aux_memory_mapper 的操作,用来给 GC 辅助用的数据结构(如:card table、prev bitmap、 next bitmap 等)创建对应的内存映射,相关操作可以查看 g1CollectedHeap 初始化部分源代码:# hotspot/src/share/vm/gc_implementation/g1/g1CollectedHeap.cpp jint G1CollectedHeap::initialize() { ...... //创建 G1 remember set // Also create a G1 rem set. _g1_rem_set = new G1RemSet(this, g1_barrier_set()); ...... // Create storage for the BOT, card table, card counts table (hot card cache) and the bitmaps. G1RegionToSpaceMapper* bot_storage = create_aux_memory_mapper("Block offset table", G1BlockOffsetSharedArray::compute_size(g1_rs.size() / HeapWordSize), G1BlockOffsetSharedArray::N_bytes); ReservedSpace cardtable_rs(G1SATBCardTableLoggingModRefBS::compute_size(g1_rs.size() / HeapWordSize)); G1RegionToSpaceMapper* cardtable_storage = create_aux_memory_mapper("Card table", G1SATBCardTableLoggingModRefBS::compute_size(g1_rs.size() / HeapWordSize), G1BlockOffsetSharedArray::N_bytes); G1RegionToSpaceMapper* card_counts_storage = create_aux_memory_mapper("Card counts table", G1BlockOffsetSharedArray::compute_size(g1_rs.size() / HeapWordSize), G1BlockOffsetSharedArray::N_bytes); size_t bitmap_size = CMBitMap::compute_size(g1_rs.size()); G1RegionToSpaceMapper* prev_bitmap_storage = create_aux_memory_mapper("Prev Bitmap", bitmap_size, CMBitMap::mark_distance()); G1RegionToSpaceMapper* next_bitmap_storage = create_aux_memory_mapper("Next Bitmap", bitmap_size, CMBitMap::mark_distance()); _hrm.initialize(heap_storage, prev_bitmap_storage, next_bitmap_storage, bot_storage, cardtable_storage, card_counts_storage); g1_barrier_set()->initialize(cardtable_storage); // Do later initialization work for concurrent refinement. _cg1r->init(card_counts_storage); ...... }因为这些辅助的结构都是一种空间换时间的思想,所以不可避免的会占用额外的内存,尤其是 G1 的 RSet 结构,当我们调大我们的堆内存,GC 所使用的内存也会不可避免的跟随增长:# -Xmx1G -Xms1G GC (reserved=164403KB, committed=164403KB) (malloc=92723KB #6540) (mmap: reserved=71680KB, committed=71680KB) # -Xmx2G -Xms2G GC (reserved=207891KB, committed=207891KB) (malloc=97299KB #12683) (mmap: reserved=110592KB, committed=110592KB) # -Xmx4G -Xms4G GC (reserved=290313KB, committed=290313KB) (malloc=101897KB #12680) (mmap: reserved=188416KB, committed=188416KB) # -Xmx8G -Xms8G GC (reserved=446473KB, committed=446473KB) (malloc=102409KB #12680) (mmap: reserved=344064KB, committed=344064KB)我们可以看到这个额外的内存开销一般在 1% - 20%之间,当然如果我们不使用 G1 收集器,这个开销是没有那么大的:# -XX:+UseSerialGC -Xmx8G -Xms8G GC (reserved=27319KB, committed=27319KB) (malloc=7KB #79) (mmap: reserved=27312KB, committed=27312KB) # -XX:+UseConcMarkSweepGC -Xmx8G -Xms8G GC (reserved=167318KB, committed=167318KB) (malloc=140006KB #373) (mmap: reserved=27312KB, committed=27312KB)我们可以看到,使用最轻量级的 UseSerialGC,GC 部分占用的内存有很明显的降低(436M -> 26.67M);使用 CMS ,GC 部分从 436M 降低到 163.39M。GC 这块内存是必须的,也是我们在使用过程中无法压缩的。停顿、吞吐量、内存占用就是 GC 中不可能同时达到的三元悖论,不同的垃圾收集器在这三者中有不同的侧重,我们应该结合自身的业务情况综合考量选择合适的垃圾收集器。由于篇幅有限,将在下篇文章继续给大家分享 追踪区域的其它内存类型(包含Compiler、Internal、Symbol、Native Memory Tracking、Arena Chunk 和 Unknown)以及 NMT 无法追踪的内存,敬请期待!参考https://wiki.openjdk.java.net/display/HotSpot/Metaspacehttps://stuefe.de/posts/metaspace/what-is-metaspacehttps://docs.oracle.com/javase/8/embedded/develop-apps-platforms/codecache.htmhttps://weread.qq.com/web/reader/53032310717f44515302749k3c5327902153c59dc0488e1https://weread.qq.com/web/reader/53032310717f44515302749ka1d32a6022aa1d0c6e83eb4https://hllvm-group.iteye.com/group/topic/21468#post-272070欢迎加入Compiler SIG交流群与大家共同交流学习编译技术相关内容,扫码添加小助手微信邀请你进入Compiler SIG交流群。
-
在鲲鹏服务器920上重装性能调优工具, 先执行了目录下的uninstall以后, 再执行安装包目录下的 ./install前面一切顺利,最后一步提示失败:
-
0.作者介绍谢依晖 :湖南大学硕士研究生在读,本科毕业于湖南大学计算机科学与技术专业1.Abstract本文调研了一些对OpenMP进行优化的方法:H. Ma, R. Zhao, X. Gao and Y. Zhang针对OpenMP程序中的barrier提出几种新功能的支持和性能的优化[1];在SC20的Booth Talks上,Johannes Doerfert分享了在LLVM上对OpenMP做的一些优化[2]。2.Barrier Optimization for OpenMP Program[1]2.1 删除冗余的barrier通过并行数据流分析,两个循环之间无数据依赖,所以S1的barrier是冗余的;parallel结束的时候有一个隐式的barrier,所以S2的barrier也是冗余的。!$omp parallel !$omp do do i = 1, 100 a(i) = d(i) end do !barrier S1 !$omp end do !$omp do do i = 1, 100 b(i) = c(i) end do !barrier S2 !$omp end do !$omp end parallel优化时,可以在该语句块加上显式的nowait(!$omp end do nowait)。2.2 实现DOACROSS并行当并行化循环的时候,如果循环依赖距离是一个常数,如下代码:do i = 2, 100 do j = 2, 100 a(i, j) = a(i - 1, j) + a(i, j-1) end do end do对外层循环i进行数据依赖检查,可以得到a[i][j]和a[i-1][j]之间的依赖距离为1。因此循环可以以DOACROSS并行的方式运行。OpenMP只实现了DOALL并行,没有与DOACROSS对应的语句。实现时,定义共享数组“_mylocks [ threadid ]”来存储每个线程的事件,定义私有变量_counter0指示当前线程正在等待的事件。数组“_mylocks”中的元素总数是线程数,每个元素表示相应线程的当前状态。实现的代码如下:int _mylocks[256]; // thread's synchronized array #pragma omp parallel { int _counter0 = l; int _my_id = omp_get_thread_num(); int _my_nprocs = omp_get_num_threads(); _mylocks[my_id] = 0; for (j_tile = 0; j_tile < N - l; j_tile += M) { if (_my_id > 0) { do { #pragma omp flush(_mylock) } while (_mylock[myid - l] < _counter0); #pragma omp flush(a, _mylock) _counter0 +=1; } #pragma omp for nowait for (i = l; i < N; i++) { for (j = j_tile; j < j_tile + M; j++){ a[i][j] = a[i - 1][j] + a[i][j - 1]; } } _mylock[myid] += 1; #pragma omp flush(a, _mylock) } }2.3 Region Barrier当线程遇到region barrier时会继续执行。但是直到其他所有线程都进入这个区域之后,它才能运行出该区域。这样的好处是允许线程继续运行而不空转,可以实现CPU的负载均衡。region barrier的实现代码如下:unsigned _counter = 0; #pragma omp parallel { {first parallel region} #pragma omp atomic _counter++; {barrier region} #pragma omp flush(counter) while(counter % omp_get_num_threads()) { #pragma omp flush(counter) } #pragma omp flush {third parallel region} }当使用region barrier时,需要保证并行域R1和R3与并行域R2无依赖关系。3.OpenMP SC20 Booth Talk Series : OpenMP compiler optimizations in LLVM [2]3.1 OpenMP运行时调用重复数据的消除double *A = malloc(size * omp_get_thread_limit()); double *B = malloc(size * omp_get_thread_limit()); #pragma omp parallel do_work(&A[omp_get_thread_num() * size]); #pragma omp parallel do_work(&B[omp_get_thread_num() * size]);示例代码中重复调用了omp_get_thread_limit()和omp_get_thread_num()函数,可以将重复调用合并至一次调用。该功能已在LLVM实现,可通过如下编译选项进行优化:$ clang deduplicate.c -g -O2 -fopenmp -Rpass=openmp-opt3.2 Tracking OpenMP Internal Control Variablesvoid foo() { #pragma omp parallel bar(); } void bar() { if (omp_in_parallel()) { ... } else { ... } }以上代码,如果omp_in_parallel()的返回值可以判断为真,那么这个if结构就可以被删除。ReferencesH. Ma, R. Zhao, X. Gao and Y. Zhang, "Barrier Optimization for OpenMP Program," 2009 10th ACIS International Conference on Software Engineering, Artificial Intelligences, Networking and Parallel/Distributed Computing, 2009, pp. 495-500, doi: 10.1109/SNPD.2009.16.https://www.openmp.org/wp-content/uploads/OpenMPOpt-in-LLVM-SC20-JD.pdf欢迎加入Compiler SIG交流群与大家共同交流学习编译技术相关内容,扫码添加小助手微信邀请你进入Compiler SIG交流群。原文转自 毕昇编译-OpenMP优化调研系列文章(1)
-
0.引言我们经常会好奇,我启动了一个 JVM,他到底会占据多大的内存?他的内存都消耗在哪里?为什么 JVM 使用的内存比我设置的 -Xmx 大这么多?我的内存设置参数是否合理?为什么我的 JVM 内存一直缓慢增长?为什么我的 JVM 会被 OOMKiller 等等,这都涉及到 JAVA 虚拟机对内存的一个使用情况,不如让我们来一探其中究竟。1.简介除去大家都熟悉的可以使用 -Xms、-Xmx 等参数设置的堆(Java Heap),JVM 还有所谓的非堆内存(Non-Heap Memory)。可以通过一张图来简单看一下 Java 进程所使用的内存情况(简略情况):非堆内存包括方法区和Java虚拟机内部做处理或优化所需的内存。方法区:在所有线程之间共享,存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码。方法区在逻辑上(虚拟机规范)是堆的一部分,但规范并不限定实现方法区的内存位置和编译代码的管理策略,所以不同的 Java 虚拟机可能有不同的实现方式,此处我们仅讨论 HotSpot。除了方法区域外,Java 虚拟机实现可能需要内存用于内部的处理或优化。例如,JIT编译器需要内存来存储从Java虚拟机代码转换的本机代码(储存在CodeCache中),以获得高性能。从 OpenJDK8 起有了一个很 nice 的虚拟机内部功能: Native Memory Tracking (NMT) 。我们可以使用 NMT 来追踪了解 JVM 的内存使用详情(即上图中的 JVM Memory 部分),帮助我们排查内存增长与内存泄漏相关的问题。2.如何使用2.1 开启 NMT默认情况下,NMT是处于关闭状态的,我们可以通过设置 JVM 启动参数来开启:-XX:NativeMemoryTracking=[off | summary | detail]。注意:启用NMT会导致5% -10%的性能开销。NMT 使用选项如下表所示:NMT 选项说明off不跟踪 JVM 本地内存使用情况。如果不指定 -XX:NativeMemoryTracking 选项则默认为off。summary仅跟踪 JVM 子系统(如:Java heap、class、code、thread等)的内存使用情况。detail除了通过 JVM 子系统跟踪内存使用情况外,还可以通过单独的 CallSite、单独的虚拟内存区域及其提交区域来跟踪内存使用情况。我们注意到,如果想使用 NMT 观察 JVM 的内存使用情况,我们必须重启 JVM 来设置 XX:NativeMemoryTracking 的相关选项,但是重启会使得我们丢失想要查看的现场,只能等到问题复现时才能继续观察。笔者试图通过一种不用重启 JVM 的方式来开启 NMT ,但是很遗憾目前没有这样的功能。JVM 启动后只有被标记为 manageable 的参数才可以动态修改或者说赋值,我们可以通过 JDK management interface (com.sun.management.HotSpotDiagnosticMXBean API) 或者 jinfo -flag 命令来进行动态修改的操作,让我们看下所有可以被修改的参数值(JDK8):java -XX:+PrintFlagsFinal | grep manageable intx CMSAbortablePrecleanWaitMillis = 100 {manageable} intx CMSTriggerInterval = -1 {manageable} intx CMSWaitDuration = 2000 {manageable} bool HeapDumpAfterFullGC = false {manageable} bool HeapDumpBeforeFullGC = false {manageable} bool HeapDumpOnOutOfMemoryError = false {manageable} ccstr HeapDumpPath = {manageable} uintx MaxHeapFreeRatio = 100 {manageable} uintx MinHeapFreeRatio = 0 {manageable} bool PrintClassHistogram = false {manageable} bool PrintClassHistogramAfterFullGC = false {manageable} bool PrintClassHistogramBeforeFullGC = false {manageable} bool PrintConcurrentLocks = false {manageable} bool PrintGC = false {manageable} bool PrintGCDateStamps = false {manageable} bool PrintGCDetails = false {manageable} bool PrintGCID = false {manageable} bool PrintGCTimeStamps = false {manageable}很显然,其中不包含 NativeMemoryTracking 。2.2 使用 jcmd 访问 NMT 数据我们可以通过 jcmd 命令来很方便的查看 NMT 相关的数据:jcmd VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]jcmd 操作 NMT 选项如下表所示:jcmd NMT 选项说明summary打印按类别汇总的摘要信息detail1.打印按类别汇总的内存使用情况2.打印虚拟内存映射3.打印按 call site 汇总的内存使用情况baseline创建一个新的内存使用状况的快照,用以进行比较summary.diff根据上一个 baseline 基线打印新的 summary 对比报告detail.diff根据上一个 baseline 基线打印新的 detail 对比报告shutdown停止NMTNMT 默认打印的报告是 KB 来进行呈现的,为了满足我们不同的需求,我们可以使用 scale=MB | GB 来更加直观的打印数据。创建 baseline 之后使用 diff 功能可以很直观地对比出两次 NMT 数据之间的差距。看到 shutdown 选项,笔者本能的一激灵,既然我们可以通过 shutdown 来关闭 NMT ,那为什么不能通过逆向 shutdown 功能来动态的开启 NMT 呢?笔者找到 shutdown 相关源码(以下都是基于 OpenJDK 8):# hotspot/src/share/vm/services/nmtDCmd.cpp void NMTDCmd::execute(DCmdSource source, TRAPS) { // Check NMT state // native memory tracking has to be on if (MemTracker::tracking_level() == NMT_off) { output()->print_cr("Native memory tracking is not enabled"); return; } else if (MemTracker::tracking_level() == NMT_minimal) { output()->print_cr("Native memory tracking has been shutdown"); return; } ...... //执行 shutdown 操作 else if (_shutdown.value()) { MemTracker::shutdown(); output()->print_cr("Native memory tracking has been turned off"); } ...... } # hotspot/src/share/vm/services/memTracker.cpp // Shutdown can only be issued via JCmd, and NMT JCmd is serialized by lock void MemTracker::shutdown() { // We can only shutdown NMT to minimal tracking level if it is ever on. if (tracking_level () > NMT_minimal) { transition_to(NMT_minimal); } } # hotspot/src/share/vm/services/nmtCommon.hpp // Native memory tracking level //NMT的追踪等级 enum NMT_TrackingLevel { NMT_unknown = 0xFF, NMT_off = 0x00, NMT_minimal = 0x01, NMT_summary = 0x02, NMT_detail = 0x03 };遗憾的是通过源码我们发现,shutdown 操作只是将 NMT 的追踪等级 tracking_level 变成了 NMT_minimal 状态(而并不是直接变成了 off 状态),注意注释:We can only shutdown NMT to minimal tracking level if it is ever on(即我们只能将NMT关闭到最低跟踪级别,如果它曾经打开)。这就导致了如果我们没有开启过 NMT ,那就没办法通过魔改 shutdown 操作逆向打开 NMT ,因为 NMT 追踪的部分内存只在 JVM 启动初始化的阶段进行记录(如在初始化堆内存分配的过程中通过 NMT_TrackingLevel level = MemTracker::tracking_level(); 来获取 NMT 的追踪等级,视等级来记录内存使用情况),JVM 启动之后再开启 NMT 这部分内存的使用情况就无法记录,所以目前来看,还是只能在重启 JVM 后开启 NMT。至于提供 shutdown 功能的原因,应该就是让用户在开启 NMT 功能之后如果想要关闭,不用再次重启 JVM 进程。shutdown 会清理虚拟内存用来追踪的数据结构,并停止一些追踪的操作(如记录 malloc 内存的分配)来降低开启 NMT 带来的性能耗损,并且通过源码可以发现 tracking_level 变成 NMT_minimal 状态后也不会再执行 jcmd VM.native_memory 命令相关的操作。2.3 虚拟机退出时获取 NMT 数据除了在虚拟机运行时获取 NMT 数据,我们还可以通过两个参数:-XX:+UnlockDiagnosticVMOptions和-XX:+PrintNMTStatistics ,来获取虚拟机退出时内存使用情况的数据(输出数据的详细程度取决于你设定的跟踪级别,如 summary/detail 等)。-XX:+UnlockDiagnosticVMOptions:解锁用于诊断 JVM 的选项,默认关闭。-XX:+PrintNMTStatistics:当启用 NMT 时,在虚拟机退出时打印内存使用情况,默认关闭,需要开启前置参数 -XX:+UnlockDiagnosticVMOptions 才能正常使用。3.NMT 内存 & OS 内存概念差异性我们可以做一个简单的测试,使用如下参数启动 JVM :-Xmx1G -Xms1G -XX:+UseG1GC -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=256m -XX:ReservedCodeCacheSize=256M -XX:NativeMemoryTracking=detail然后使用 NMT 查看内存使用情况(因各环境资源参数不一样,部分未明确设置数据可能由虚拟机根据资源自行计算得出,以下数据仅供参考):jcmd VM.native_memory detailNMT 会输出如下日志:Native Memory Tracking: Total: reserved=2813709KB, committed=1497485KB - Java Heap (reserved=1048576KB, committed=1048576KB) (mmap: reserved=1048576KB, committed=1048576KB) - Class (reserved=1056899KB, committed=4995KB) (classes #442) (malloc=131KB #259) (mmap: reserved=1056768KB, committed=4864KB) - Thread (reserved=258568KB, committed=258568KB) (thread #127) (stack: reserved=258048KB, committed=258048KB) (malloc=390KB #711) (arena=130KB #234) - Code (reserved=266273KB, committed=4001KB) (malloc=33KB #309) (mmap: reserved=266240KB, committed=3968KB) - GC (reserved=164403KB, committed=164403KB) (malloc=92723KB #6540) (mmap: reserved=71680KB, committed=71680KB) - Compiler (reserved=152KB, committed=152KB) (malloc=4KB #36) (arena=148KB #21) - Internal (reserved=14859KB, committed=14859KB) (malloc=14827KB #3632) (mmap: reserved=32KB, committed=32KB) - Symbol (reserved=1423KB, committed=1423KB) (malloc=936KB #111) (arena=488KB #1) - Native Memory Tracking (reserved=330KB, committed=330KB) (malloc=118KB #1641) (tracking overhead=211KB) - Arena Chunk (reserved=178KB, committed=178KB) (malloc=178KB) - Unknown (reserved=2048KB, committed=0KB) (mmap: reserved=2048KB, committed=0KB) ......大家可能会发现 NMT 所追踪的内存(即 JVM 中的 Reserved、Committed)与操作系统 OS (此处指Linux)的内存概念存在一定的差异性。首先按我们理解的操作系统的概念:操作系统对内存的分配管理典型地分为两个阶段:保留(reserve)和提交(commit)。保留阶段告知系统从某一地址开始到后面的dwSize大小的连续虚拟内存需要供程序使用,进程其他分配内存的操作不得使用这段内存;提交阶段将虚拟地址映射到对应的真实物理内存中,这样这块内存就可以正常使用 [1]。如果使用 top 或者 smem 等命令查看刚才启动的 JVM 进程会发现:top PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 36257 dou+ 20 0 10.8g 54200 17668 S 99.7 0.0 13:04.15 java此时疑问就产生了,为什么 NMT 中的 committed ,即日志详情中 Total: reserved=2813709KB, committed=1497485KB 中的 1497485KB 与 top 中 RES 的大小54200KB 存在如此大的差异?使用 man 查看 top 中 RES 的概念(不同版本 Linux 可能不同):RES -- Resident Memory Size (KiB) A subset of the virtual address space (VIRT) representing the non-swapped physical memory a task is currently using. It is also the sum of the RSan, RSfd and RSsh fields. It can include private anonymous pages, private pages mapped to files (including program images and shared libraries) plus shared anonymous pages. All such memory is backed by the swap file represented separately under SWAP. Lastly, this field may also include shared file-backed pages which, when modified, act as a dedicated swap file and thus will never impact SWAP.RES 表示任务当前使用的非交换物理内存(此时未发生swap),那按对操作系统 commit 提交内存的理解,这两者貌似应该对上,为何现在差距那么大呢?笔者一开始猜测是 JVM 的 uncommit 机制(如 JEP 346[2],支持 G1 在空闲时自动将 Java 堆内存返回给操作系统,BiSheng JDK 对此做了增强与改进[3])造成的,JVM 在 uncommit 将内存返还给 OS 之后,NMT 没有除去返还的内存导致统计错误。但是在翻阅了源码之后发现,G1 在 shrink 缩容的时候,通常调用链路如下:G1CollectedHeap::shrink ->G1CollectedHeap::shrink_helper ->HeapRegionManager::shrink_by ->HeapRegionManager::uncommit_regions ->G1PageBasedVirtualSpace::uncommit ->G1PageBasedVirtualSpace::uncommit_internal ->os::uncommit_memory忽略细节,uncommit 会在最后调用 os::uncommit_memory ,查看 os::uncommit_memory 源码:bool os::uncommit_memory(char* addr, size_t bytes) { bool res; if (MemTracker::tracking_level() > NMT_minimal) { Tracker tkr = MemTracker::get_virtual_memory_uncommit_tracker(); res = pd_uncommit_memory(addr, bytes); if (res) { tkr.record((address)addr, bytes); } } else { res = pd_uncommit_memory(addr, bytes); } return res; }可以发现在返还 OS 内存之后,MemTracker 是进行了统计的,所以此处的误差不是由 uncommit 机制造成的。既然如此,那又是由什么原因造成的呢?笔者在追踪 JVM 的内存分配逻辑时发现了一些端倪,此处以Code Cache(存放 JVM 生成的 native code、JIT编译、JNI 等都会编译代码到 native code,其中 JIT 生成的 native code 占用了 Code Cache 的绝大部分空间)的初始化分配为例,其大致调用链路为下:InitializeJVM ->Thread::vreate_vm ->init_globals ->codeCache_init ->CodeCache::initialize ->CodeHeap::reserve ->VirtualSpace::initialize ->VirtualSpace::initialize_with_granularity ->VirtualSpace::expand_by ->os::commit_memory查看 os::commit_memory 相关源码:bool os::commit_memory(char* addr, size_t size, size_t alignment_hint, bool executable) { bool res = os::pd_commit_memory(addr, size, alignment_hint, executable); if (res) { MemTracker::record_virtual_memory_commit((address)addr, size, CALLER_PC); } return res; }我们发现 MemTracker 在此记录了 commit 的内存供 NMT 用以统计计算,继续查看 os::pd_commit_memory 源码,可以发现其调用了 os::Linux::commit_memory_impl 函数。查看 os::Linux::commit_memory_impl 源码:int os::Linux::commit_memory_impl(char* addr, size_t size, bool exec) { int prot = exec ? PROT_READ|PROT_WRITE|PROT_EXEC : PROT_READ|PROT_WRITE; uintptr_t res = (uintptr_t) ::mmap(addr, size, prot, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0); if (res != (uintptr_t) MAP_FAILED) { if (UseNUMAInterleaving) { numa_make_global(addr, size); } return 0; } int err = errno; // save errno from mmap() call above if (!recoverable_mmap_error(err)) { warn_fail_commit_memory(addr, size, exec, err); vm_exit_out_of_memory(size, OOM_MMAP_ERROR, "committing reserved memory."); } return err; }问题的原因就在 uintptr_t res = (uintptr_t) ::mmap(addr, size, prot, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0); 这段代码上。我们发现,此时申请内存执行的是 mmap 函数,并且传递的 port 参数是 PROT_READ|PROT_WRITE|PROT_EXEC 或 PROT_READ|PROT_WRITE ,使用 man 查看 mmap ,其中相关描述为:The prot argument describes the desired memory protection of the mapping (and must not conflict with the open mode of the file). It is either PROT_NONE or the bitwise OR of one or more of the following flags: PROT_EXEC Pages may be executed. PROT_READ Pages may be read. PROT_WRITE Pages may be written. PROT_NONE Pages may not be accessed.由此我们可以看出,JVM 中所谓的 commit 内存,只是将内存 mmaped 映射为可读可写可执行的状态!而在 Linux 中,在分配内存时又是 lazy allocation 的机制,只有在进程真正访问时才分配真实的物理内存。所以 NMT 中所统计的 committed 并不是对应的真实的物理内存,自然与 RES 等统计方式无法对应起来。所以 JVM 为我们提供了一个参数 -XX:+AlwaysPreTouch,使我们可以在启动之初就按照内存页粒度都访问一遍 Heap,强制为其分配物理内存以减少运行时再分配内存造成的延迟(但是相应的会影响 JVM 进程初始化启动的时间),查看相关代码:void os::pretouch_memory(char* start, char* end) { for (volatile char *p = start; p < end; p += os::vm_page_size()) { *p = 0; } }让我们来验证下,开启 -XX:+AlwaysPreTouch 前后的效果。NMT 的 heap 地址范围:Virtual memory map: [0x00000000c0000000 - 0x0000000100000000] reserved 1048576KB for Java Heap from [0x0000ffff93ea36d8] ReservedHeapSpace::ReservedHeapSpace(unsigned long, unsigned long, bool, char*)+0xb8 [0x0000ffff93e67f68] Universe::reserve_heap(unsigned long, unsigned long)+0x2d0 [0x0000ffff93898f28] G1CollectedHeap::initialize()+0x188 [0x0000ffff93e68594] Universe::initialize_heap()+0x15c [0x00000000c0000000 - 0x0000000100000000] committed 1048576KB from [0x0000ffff938bbe8c] G1PageBasedVirtualSpace::commit_internal(unsigned long, unsigned long)+0x14c [0x0000ffff938bc08c] G1PageBasedVirtualSpace::commit(unsigned long, unsigned long)+0x11c [0x0000ffff938bf774] G1RegionsLargerThanCommitSizeMapper::commit_regions(unsigned int, unsigned long)+0x5c [0x0000ffff93943f54] HeapRegionManager::commit_regions(unsigned int, unsigned long)+0x7c对应该地址的/proc/{pid}/smaps://开启前 //开启后 c0000000-100080000 rw-p 00000000 00:00 0 c0000000-100080000 rw-p 00000000 00:00 0 Size: 1049088 kB Size: 1049088 kB KernelPageSize: 4 kB KernelPageSize: 4 kB MMUPageSize: 4 kB MMUPageSize: 4 kB Rss: 792 kB Rss: 1049088 kB Pss: 792 kB Pss: 1049088 kB Shared_Clean: 0 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Clean: 0 kB Private_Dirty: 792 kB Private_Dirty: 1049088 kB Referenced: 792 kB Referenced: 1048520 kB Anonymous: 792 kB Anonymous: 1049088 kB LazyFree: 0 kB LazyFree: 0 kB AnonHugePages: 0 kB AnonHugePages: 0 kB ShmemPmdMapped: 0 kB ShmemPmdMapped: 0 kB Shared_Hugetlb: 0 kB Shared_Hugetlb: 0 kB Private_Hugetlb: 0 kB Private_Hugetlb: 0 kB Swap: 0 kB Swap: 0 kB SwapPss: 0 kB SwapPss: 0 kB Locked: 0 kB Locked: 0 kB VmFlags: rd wr mr mw me ac VmFlags: rd wr mr mw me ac对应的/proc/{pid}/status://开启前 //开启后 ... ... VmHWM: 54136 kB VmHWM: 1179476 kB VmRSS: 54136 kB VmRSS: 1179476 kB ... ... VmSwap: 0 kB VmSwap: 0 kB ...开启参数后的 top:PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 85376 dou+ 20 0 10.8g 1.1g 17784 S 99.7 0.4 14:56.31 java观察对比我们可以发现,开启 AlwaysPreTouch 参数后,NMT 统计的 commited 已经与 top 中的 RES 差不多了,之所以不完全相同是因为该参数只能 Pre-touch 分配 Java heap 的物理内存,至于其他的非 heap 的内存,还是受到 lazy allocation 机制的影响。同理我们可以简单看下 JVM 的 reserve 机制:# hotspot/src/share/vm/runtime/os.cpp char* os::reserve_memory(size_t bytes, char* addr, size_t alignment_hint, MEMFLAGS flags) { char* result = pd_reserve_memory(bytes, addr, alignment_hint); if (result != NULL) { MemTracker::record_virtual_memory_reserve((address)result, bytes, CALLER_PC); MemTracker::record_virtual_memory_type((address)result, flags); } return result; } # hotspot/src/os/linux/vm/os_linux.cpp char* os::pd_reserve_memory(size_t bytes, char* requested_addr, size_t alignment_hint) { return anon_mmap(requested_addr, bytes, (requested_addr != NULL)); } static char* anon_mmap(char* requested_addr, size_t bytes, bool fixed) { ...... addr = (char*)::mmap(requested_addr, bytes, PROT_NONE, flags, -1, 0); ...... } reserve 通过 mmap(requested_addr, bytes, PROT_NONE, flags, -1, 0); 来将内存映射为 PROT_NONE,这样其他的 mmap/malloc 等就不能调用使用,从而达到了 guard memory 或者说 guard pages 的目的。OpenJDK 社区其实也注意到了 NMT 内存与 OS 内存差异性的问题,所以社区也提出了相应的 Enhancement 来增强功能:JDK-8249666[4] :目前 NMT 将分配的内存显示为 Reserved 或 Committed。而在 top 或 pmap 的输出中,首次使用(即 touch)之前 Reserved 和 Committed 的内存都将显示为 Virtual memory。只有在内存页(通常是4k)首次写入后,它才会消耗物理内存,并出现在 top/pmap 输出的 “常驻内存”(即 RSS)中。当前NMT输出的主要问题是,它无法区分已 touch 和未 touch 的 Committed 内存。该 Enhancement 提出可以使用 mincore() [5]来查找 NMT 的 Committed 中 RSS 的部分,mincore() 系统调用让一个进程能够确定一块虚拟内存区域中的分页是否驻留在物理内存中。mincore()已在JDK-8191369 NMT:增强线程堆栈跟踪中实现,需要将其扩展到所有其他类型的内存中(如 Java 堆)。遗憾的是该 Enhancement 至今仍是 Unresolved 状态。JDK-8191369[6] :1 中提到的 NMT:增强线程堆栈跟踪。使用 mincore() 来追踪驻留在物理内存中的线程堆栈的大小,用以解决线程堆栈追踪时有时会夸大内存使用情况的痛点。该 Enhancement 已经在 JDK11 中实现。参考https://weread.qq.com/web/reader/53032310717f44515302749k37632cd021737693cfc7149http://openjdk.java.net/jeps/346https://gitee.com/openeuler/bishengjdk-8/wikis/G1GCå åä¼¸ç¼©ç¹æ§ä»ç»?sort_id=3340035https://bugs.openjdk.org/browse/JDK-8249666https://man7.org/linux/man-pages/man2/mincore.2.htmlhttps://bugs.openjdk.org/browse/JDK-8191369
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签