-
【版本信息】不涉及【问题现象及处理方法】之前介绍的性能问题处理套路中查杀烂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作业时间
-
大家在了解优化目标后,那接下来应该从哪些方面入手呢?本文主要侧重于理论分析,我们从整体上看一下 Java 性能优化都有哪些可以遵循的规律。本文主讲理论。关于实践,后续的文章会用较多的案例来细化本文的知识点,适合反复思考和归纳。性能优化根据优化的类别,分为业务优化和技术优化。业务优化产生的效果也是非常大的,但它属于产品和管理的范畴。同作为程序员,在平常工作中,我们面对的优化方式,主要是通过一系列的技术手段,来完成对既定的优化目标。这一系列的技术手段,我大体归纳为如图以下 7 类:可以看到,优化方式集中在对计算资源和存储资源的规划上。优化方法中有多种用空间换时间的方式,但只照顾计算速度,而不考虑复杂性和空间问题,也是不可取的。我们要做的,就是在照顾性能的前提下,达到资源利用的最优状态。1、复用优化在写代码的时候,你会发现有很多重复的代码可以提取出来,做成公共的方法。这样,在下次用的时候,就不用再费劲写一遍了。这种思想就是复用。上面的描述是编码逻辑上的优化,对于数据存取来说,有同样的复用情况。无论是在生活中还是编码中,重复的事情一直在发生,如果没有复用,工作和生活就会比较累。在软件系统中,谈到数据复用,我们首先想到的就是缓冲和缓存。注意这两个词的区别,它们的意义是完全不同的,很多同学很容易搞混,在这里简单地介绍一下。缓冲(Buffer),常见于对数据的暂存,然后批量传输或者写入。多使用顺序方式,用来缓解不同设备之间频繁地、缓慢地随机写,缓冲主要针对的是写操作。缓存(Cache),常见于对已读取数据的复用,通过将它们缓存在相对高速的区域,缓存主要针对的是读操作。与之类似的,是对于对象的池化操作,比如数据库连接池、线程池等,在 Java 中使用得非常频繁。由于这些对象的创建和销毁成本都比较大,我们在使用之后,也会将这部分对象暂时存储,下次用的时候,就不用再走一遍耗时的初始化操作了。2、计算优化并行执行现在的 CPU 发展速度很快,绝大多数硬件,都是多核。要想加快某个任务的执行,最快最优的解决方式,就是让它并行执行。并行执行有以下三种模式。第一种模式是多机,采用负载均衡的方式,将流量或者大的计算拆分成多个部分,同时进行处理。比如,Hadoop 通过 MapReduce 的方式,把任务打散,多机同时进行计算。第二种模式是采用多进程。比如 Nginx,采用 NIO 编程模型,Master 统一管理 Worker 进程,然后由 Worker 进程进行真正的请求代理,这也能很好地利用硬件的多个 CPU。第三种模式是使用多线程,这也是 Java 程序员接触最多的。比如 Netty,采用 Reactor 编程模型,同样使用 NIO,但它是基于线程的。Boss 线程用来接收请求,然后调度给相应的 Worker 线程进行真正的业务计算。像 Golang 这样的语言,有更加轻量级的协程(Coroutine),协程是一种比线程更加轻量级的存在,但目前在 Java 中还不太成熟,就不做过多介绍了,但本质上,它也是对于多核的应用,使得任务并行执行。变同步为异步再一种对于计算的优化,就是变同步为异步,这通常涉及编程模型的改变。同步方式,请求会一直阻塞,直到有成功,或者失败结果的返回。虽然它的编程模型简单,但应对突发的、时间段倾斜的流量,问题就特别大,请求很容易失败。异步操作可以方便地支持横向扩容,也可以缓解瞬时压力,使请求变得平滑。同步请求,就像拳头打在钢板上;异步请求,就像拳头打在海绵上。你可以想象一下这个过程,后者肯定是富有弹性的,体验更加友好。惰性加载最后一种,就是使用一些常见的设计模式来优化业务,提高体验,比如单例模式、代理模式等。举个例子,在绘制 Swing 窗口的时候,如果要显示比较多的图片,就可以先加载一个占位符,然后通过后台线程慢慢加载所需要的资源,这就可以避免窗口的僵死。3、结果集优化接下来介绍一下对结果集的优化。举个比较直观的例子,我们都知道 XML 的表现形式是非常好的,那为什么还有 JSON 呢?除了书写要简单一些,一个重要的原因就是它的体积变小了,传输效率和解析效率变高了,像 Google 的 Protobuf,体积就更小了一些。虽然可读性降低,但在一些高并发场景下(如 RPC),能够显著提高效率,这是典型的对结果集的优化。这是由于我们目前的 Web 服务,都是 C/S 模式。数据从服务器传输到客户端,需要分发多份,这个数据量是急剧膨胀的,每减少一小部分存储,都会有比较大的传输性能和成本提升。像 Nginx,一般都会开启 GZIP 压缩,使得传输的内容保持紧凑。客户端只需要一小部分计算能力,就可以方便解压。由于这个操作是分散的,所以性能损失是固定的。了解了这个道理,我们就能看到对于结果集优化的一般思路,你要尽量保持返回数据的精简。一些客户端不需要的字段,那就在代码中,或者直接在 SQL 查询中,就把它去掉。对于一些对时效性要求不高,但对处理能力有高要求的业务。我们要吸取缓冲区的经验,尽量减少网络连接的交互,采用批量处理的方式,增加处理速度。结果集合很可能会有二次使用,你可能会把它加入缓存中,但依然在速度上有所欠缺。这个时候,就需要对数据集合进行处理优化,采用索引或者 Bitmap 位图等方式,加快数据访问速度。4、资源冲突优化我们在平常的开发中,会涉及很多共享资源。这些共享资源,有的是单机的,比如一个 HashMap;有的是外部存储,比如一个数据库行;有的是单个资源,比如 Redis 某个 key 的Setnx;有的是多个资源的协调,比如事务、分布式事务等。现实中的性能问题,和锁相关的问题是非常多的。大多数我们会想到数据库的行锁、表锁、Java 中的各种锁等。在更底层,比如 CPU 命令级别的锁、JVM 指令级别的锁、操作系统内部锁等,可以说无处不在。只有并发,才能产生资源冲突。也就是在同一时刻,只能有一个处理请求能够获取到共享资源。解决资源冲突的方式,就是加锁。再比如事务,在本质上也是一种锁。按照锁级别,锁可分为乐观锁和悲观锁,乐观锁在效率上肯定是更高一些;按照锁类型,锁又分为公平锁和非公平锁,在对任务的调度上,有一些细微的差别。对资源的争用,会造成严重的性能问题,所以会有一些针对无锁队列之类的研究,对性能的提升也是巨大的。5、算法优化算法能够显著提高复杂业务的性能,但在实际的业务中,往往都是变种。由于存储越来越便宜,在一些 CPU 非常紧张的业务中,往往采用空间换取时间的方式,来加快处理速度。算法属于代码调优,代码调优涉及很多编码技巧,需要使用者对所使用语言的 API 也非常熟悉。有时候,对算法、数据结构的灵活使用,也是代码优化的一个重要内容。比如,常用的降低时间复杂度的方式,就有递归、二分、排序、动态规划等。一个优秀的实现,比一个拙劣的实现,对系统的影响是非常大的。比如,作为 List 的实现,LinkedList 和 ArrayList 在随机访问的性能上,差了好几个数量级;又比如,CopyOnWriteList 采用写时复制的方式,可以显著降低读多写少场景下的锁冲突。而什么时候使用同步,什么时候是线程安全的,也对我们的编码能力有较高的要求。这部分的知识,就需要我们在平常的工作中注意积累,后面的课时中,也会挑比较重要的知识点穿插讲解。6、高效实现在平时的编程中,尽量使用一些设计理念良好、性能优越的组件。比如,有了 Netty,就不用再选择比较老的 Mina 组件。而在设计系统时,从性能因素考虑,就不要选 SOAP 这样比较耗时的协议。再比如,一个好的语法分析器(比如使用 JavaCC),其效率会比正则表达式高很多。总之,如果通过测试分析,找到了系统的瓶颈点,就要把关键的组件,使用更加高效的组件进行替换。在这种情况下,适配器模式是非常重要的。这也是为什么很多公司喜欢在现有的组件之上,再抽象一层自己的;而当在底层组件进行切换的时候,上层的应用并无感知。7、JVM优化因为 Java 是运行在 JVM 虚拟机之上,它的诸多特性,就要受到 JVM 的制约。对 JVM 虚拟机进行优化,也能在一定程度上能够提升 JAVA 程序的性能。如果参数配置不当,甚至会造成 OOM 等比较严重的后果。目前被广泛使用的垃圾回收器是 G1,通过很少的参数配置,内存即可高效回收。CMS 垃圾回收器已经在 Java 14 中被移除,由于它的 GC 时间不可控,有条件应该尽量避免使用。JVM 性能调优涉及方方面面的取舍,往往是牵一发而动全身,需要全盘考虑各方面的影响。所以了解 JVM 内部的一些运行原理,还是特别重要的,它有益于我们加深对代码更深层次的理解,帮助我们书写出更高效的代码。
-
当我使用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
-
环境鲲鹏 920 128 核Bisheng JDK 17.0.1 / 17.0.4运行 BenchmarkSQL + ShardingSphere-JDBC网卡队列绑核 0-15,BenchmarkSQL 绑核 16-127。现象概率性出现运行 BenchmarkSQL us 较高(几乎压满 CPU 核),sy 较低。之前使用的是 GraalVM EE,本来以为是 GraalVM EE 才有的现象,后续测试发现使用 Bisheng JDK 17 也有相似现象。多次启动 BenchmarkSQL 进程,性能时好时坏。正常情况下,使用 BenchmarkSQL + ShardingSphere-JDBC 性能可以达到 200 万 tpmC 以上。最近启动进程时有较高概率发生性能异常,性能相比正常情况下降约 30%。Bisheng JDK 17 现象正常情况us 相对较低,网卡中断几乎跑满前 16 核。正常情况下 tpmC 在 200 万左右。异常现象最近测试发现,经常出现 us 较高,中断相比性能正常时更低。tpmC 在 160 万左右。性能下降至正常的 70% 左右。升级 Bisheng JDK 17.0.4 问题未解决async-profiler 采样对比正常情况与异常情况的采样,表面现象为计算开销增加,在代码路径上没有看出其他异常。 GraalVM EE 现象偶发现象:CPU us 特别高,sy 较低。退出压测进程重新运行后现象消失。6 节点测试中,该现象频繁出现GraalVM 发生该现象时对性能影响非常明显,tpmC 相比正常几乎减半。
-
本文针对静态图模式,介绍如何运用Dump工具对网络数据进行分析。 分为异步dump和同步dump两种方式。注:推荐用异步dump。MindSpore默认开启内存复用,而同步dump会关闭内存复用,可能会影响训练状态。一、异步Dump大型网络(如Bert Large)使用同步Dump时会导致内存溢出,MindSpore通过异步Dump提供了大型网络的调试能力。1、创建json格式的配置文件json文件名称和位置可自定义设置。{ "common_dump_settings": { "dump_mode": 0, "path": "/absolute_path", "net_name": "ResNet50", "iteration": "0", "input_output": 0, "kernels": ["Default/Conv-op12"], "support_device": [0,1,2,3,4,5,6,7], "op_debug_mode": 0 }}参数说明:dump_mode:设置成0,表示Dump出改网络中的所有算子;设置成1,表示Dump"kernels"里面指定的算子。path:Dump保存数据的绝对路径。net_name:自定义的网络名称,例如:”ResNet50”。iteration:指定需要Dump的迭代。类型为str,用“|”分离要保存的不同区间的step的数据。如”0|5-8|100-120”表示Dump参数初始值,第1个,第6个到第9个, 第101个到第121个step的数据。指定“all”,表示Dump所有迭代的数据。input_output:设置成0,表示Dump出算子的输入和算子的输出;设置成1,表示Dump出算子的输入;设置成2,表示Dump出算子的输出。kernels:算子的名称列表。开启IR保存开关context.set_context(save_graphs=True)并执行用例,从生成的trace_code_graph_{graph_id}IR文件中获取算子名称。kernels仅支持TBE算子、AiCPU算子、通信算子,若设置成通信算子的名称,将会Dump出通信算子的输入算子的数据。详细说明可以参照教程:如何保存IR。support_device:支持的设备,默认设置成0到7即可;在分布式训练场景下,需要dump个别设备上的数据,可以只在support_device中指定需要Dump的设备Id。注:本示例是单卡任务,用默认配置即可。enable:开启异步Dump,如果同时开启同步Dump和异步Dump,那么只有同步Dump会生效。op_debug_mode:该属性用于算子溢出调试,设置成0,表示不开启溢出;设置成1,表示开启AiCore溢出检测;设置成2,表示开启Atomic溢出检测;设置成3,表示开启全部溢出检测功能。在Dump数据的时候请设置成0,若设置成其他值,则只会Dump溢出算子的数据。2、设置Dump环境变量export MINDSPORE_DUMP_CONFIG=/home/ma-user/xxx/data_dump.json此处路径应为json配置文件的绝对路径。3、脚本修改设置context.set_context(reserve_class_name_in_scope=False),避免Dump文件名称过长导致Dump数据文件生成失败。4、运行脚本启动命令:python MindSpore_1P.py异步Dump保存的数据目录结构如下所示:{path}/ |-- {device_id}/ |-- {new_name}_graph_{graph_id}/ |-- {graph_id}/ |-- {iteration}/ |-- {op_type}.{op_name}.{task_id}.{timestamp} … |-- graphs/ ms_output_trace_code_graph_{graph_id}.pb ms_output_trace_code_graph_{graph_id}.ir |-- execution_order/ ms_execution_order_graph_{graph_id}.csv |-- .metadata/ data_dump.jsonpath:data_dump.json文件中设置的绝对路径。net_name:data_dump.json文件中设置的网络名称。device_id:训练的卡号。graph_id:训练的图标号。iteration:训练的轮次。op_type:算子类型。op_name:算子名称。taskid:任务标号。timestamp:时间戳。5、异步Dump数据分析样例以Resnet50脚本为例:def _conv2d(in_channel, out_channel, kernel_size, stride=1, padding=0): scale = math.sqrt(1/(in_channel*kernel_size*kernel_size)) if padding == 0: return nn.Conv2d(in_channel, out_channel, kernel_size=kernel_size, stride=stride, padding=padding, pad_mode='same', weight_init=mindspore.common.initializer.Uniform(scale=scale)) else: return nn.Conv2d(in_channel, out_channel, kernel_size=kernel_size, stride=stride, padding=padding, pad_mode='pad', weight_init=mindspore.common.initializer.Uniform(scale=scale))...class ResNet(nn.Cell): def __init__(self, num_blocks, num_classes=10): super(ResNet, self).__init__() self.in_planes = 64 self.conv1 = _conv2d(3, 64, kernel_size=3, stride=1, padding=1) self.bn1 = nn.BatchNorm2d(64) self.relu = nn.ReLU() self.layer1 = self._make_layer(64, num_blocks[0], stride=1) self.layer2 = self._make_layer(128, num_blocks[1], stride=2) self.layer3 = self._make_layer(256, num_blocks[2], stride=2) self.layer4 = self._make_layer(512, num_blocks[3], stride=2) self.avgpool2d = nn.AvgPool2d(kernel_size=4, stride=4) self.reshape = mindspore.ops.Reshape() self.linear = _dense(2048, num_classes) self.print = P.Print() self.print_grad = P.InsertGradientOf(self.save_gradient) def save_gradient(self, dout): return dout def _make_layer(self, planes, num_blocks, stride): strides = [stride] + [1]*(num_blocks-1) layers = [] for stride in strides: layers.append(ResidualBlock(self.in_planes, planes, stride)) self.in_planes = EXPANSION*planes return nn.SequentialCell(*layers) def construct(self, x): x = self.conv1(x) out = self.relu(self.bn1(x)) out = self.layer1(out) out = self.layer2(out) out = self.layer3(out) out = self.layer4(out) out = self.avgpool2d(out) out = self.reshape(out, (out.shape[0], 2048)) out = self.linear(out) return out若用户想查看脚本中第一个卷积算子的权重 :x = self.conv1(x)(1) 查找算子对应的数据文件执行完训练网络后,可以从最终执行图(ms_output_trace_code_graph_{graph_id}.ir文件)中查找到该行代码所对应的多个算子信息,文件内容如下所示:...%4(equivoutput) = Conv2D(%1, %3) {instance name: conv2d} primitive_attrs: {visited: true, pri_format: NC1HWC0, IsFeatureMapInputList: (0), out_channel: 64, kernel_size: (3, 3), IsFeatureMapOutput: true, pad_mode: pad, stride: (1, 1, 1, 1), mode: 1, pad: (1, 1, 1, 1), pad_list: (1, 1, 1, 1), group: 1, format: NCHW, dilation: (1, 1, 1, 1), input_names: [x, w], output_names: [output], groups: 1}: (, ) -> (): (, ) -> (): (Default/network/_backbone/conv1/Conv2D-op515)# In file /home/ma-user/miniconda3/envs/MindSpore-python3.7-aarch64/lib/python3.7/site-packages/mindspore/nn/layer/conv.py(258)/ output = self.conv2d(x, self.weight)/# In file /home/ma-user/work/xxx/Resnet50_cifar10/youhuahou/src/resnet.py(88)/ x = self.conv1(x)/...以上所示文件包括:算子在Host侧(第一行)和Device侧(第二行,有些算子可能不存在)的输入输出情况。: (, ) -> (): (, ) -> ()算子名称Default/network/_backbone/conv1/Conv2D-op515算子对应训练脚本代码# In file /home/ma-user/work/xxx/Resnet50_cifar10/src/resnet.py(88)/ x = self.conv1(x)/根据算子名称中的op515,在Dump生成的数据文件目录({iteration})中,查找对应的Tensor数据文件。搜索到相应的文件名:Conv2D.Default_network__backbone_conv1_Conv2D-op515.58.16.1624605159829577(2)使用海思Run包中提供的msaccucmp.py ,解析Dump出来的文件 。注:不同的环境上msaccucmp.py文件所在的路径可能不同,可以通过find命令进行查找:find ${run_path} -name "msaccucmp.py"run包的安装路径。(3)找到msaccucmp.py后,到/absolute_path目录下,运行如下命令解析Dump数据:python ${The absolute path of msaccucmp.py} convert -d {file path of dump} -out {file path of output}数据在Device侧的格式可能和Host侧计算图中的定义不同,异步Dump的数据格式为Device侧格式,如果想要转为Host侧格式,可以参考如何进行dump数据文件Format转换。由于转换中存在FRACTAL_Z to NCHW格式的转换,工具当前未支持,因此需要编写自定义转换脚本,可使用示例代码包中的convert_FRACTAL_Z_to_NCHW.py脚本进行转换。将convert_FRACTAL_Z_to_NCHW.py文件放在新建的format_convert文件夹中。-c后指定为format_convert目录的上一层目录。执行: (示例中format_convert文件夹新建在../目录下,也可以创建在其他目录下)python /usr/local/Ascend/toolkit/tools/operator_cmp/compare/msaccucmp.py convert -d ./Conv2D.Default_network__backbone_conv1_Conv2D-op515.58.16.1624605159829577 -out ../output/ -f NCHW -c ../在./output下生成该算子的所有输入输出数据。每个数据以.npy后缀的文件保存,生成结果如下:Conv2D.Default_network__backbone_conv1_Conv2D-op515.58.16.1624605159829577.input.0.128x3x32x32.npyConv2D.Default_network__backbone_conv1_Conv2D-op515.58.16.1624605159829577.input.1.64x3x3x3.npyConv2D.Default_network__backbone_conv1_Conv2D-op515.58.16.1624605159829577.output.0.128x64x32x32.npy在文件名的末尾可以看到该文件是算子的第几个输入或输出,再结合维度信息可以判断数据的含义:例如,上面三个文件依次表示该卷积算子的输入、权重和输出。(4)通过numpy.load接口读取要查看的数据在终端依次输入:python->import numpy->numpy.load("Conv2D.Default_network__backbone_conv1_Conv2D-op515.58.16.1624605159829577.input.1.64x3x3x3.npy")输出数据: array([[[[ 0.01878 , 0.0828 , 0.03955 ], [ 0.01727 , -0.02939 , 0.05615 ], [-0.02402 , 0.1508 , 0.1785 ]], ... [[ 0.1145 , 0.1186 , 0.1643 ], [-0.148 , -0.1088 , 0.0935 ], [-0.117 , -0.0822 , -0.1283 ]]]], dtype=float16)二、同步Dump1、创建json格式的配置文件json文件名称和位置可自定义设置。{ "common_dump_settings": { "dump_mode": 0, "path": "/absolute_path", "net_name": "ResNet50", "iteration": "0", "input_output": 0, "kernels": ["Default/Conv-op12"], "support_device": [0,1,2,3,4,5,6,7] }, "e2e_dump_settings": { "enable": true, "trans_flag": true }}参数说明:dump_mode:设置成0,表示Dump出该网络中的所有算子;设置成1,表示Dump"kernels"里面指定的算子。path:Dump保存数据的绝对路径。net_name:自定义的网络名称,例如:”ResNet50”。iteration:指定需要Dump数据的迭代。类型为str,用“|”分离要保存的不同区间的step的数据。如”0|5-8|100-120”表示Dump参数初始值,第1个,第6个到第9个, 第101个到第121个step的数据。指定“all”,表示Dump所有迭代的数据。input_output:设置成0,表示Dump出算子的输入和算子的输出;设置成1,表示Dump出算子的输入;设置成2,表示Dump出算子的输出。该配置参数仅支持Ascend和CPU,GPU只能Dump算子的输出。kernels:算子的名称列表。开启IR保存开关context.set_context(save_graphs=True)并执行用例,从生成的IR文件trace_code_graph_{graph_id}中获取算子名称。详细说明可以参照教程:如何保存IR。support_device:支持的设备,默认设置成0到7即可;在分布式训练场景下,需要dump个别设备上的数据,可以只在support_device中指定需要Dump的设备Id。该配置参数在CPU上无效,因为CPU下没有device这个概念。注:本示例是单卡任务,用默认配置即可。enable:开启E2E Dump,如果同时开启同步Dump和异步Dump,那么只有同步Dump会生效。trans_flag:开启格式转换。将设备上的数据格式转换成NCHW格式。若为True,则数据会以Host侧的4D格式(NCHW)格式保存;若为False,则保留Device侧的数据格式。该配置参数在CPU上无效,因为CPU上没有format转换。2、设置Dump环境变量export MINDSPORE_DUMP_CONFIG=/home/ma-user/xxx/data_dump.json此处路径应为json配置文件的绝对路径。3、脚本修改设置model.train中的dataset_sink_mode参数为False 。同步模式下Dump数据,必须采用非数据下沉模式,以保证可以获取每个step的Dump数据。设置context.set_context(reserve_class_name_in_scope=False)。避免Dump文件名称过长导致Dump数据文件生成失败。4、运行脚本启动命令:python MindSpore_1P.py同步Dump保存的数据目录结构如下所示:{path}/ |-- {net_name}/ |-- {device_id}/ |-- iteration_{iteration}/ -- {op_name}_{input_output_index}_{shape}_{data_type}_{format}.bin … |-- graphs/ ms_output_trace_code_graph_{graph_id}.pb ms_output_trace_code_graph_{graph_id}.ir |-- execution_order/ ms_execution_order_graph_{graph_id}.csv |-- .metadata/ data_dump.jsonpath:data_dump.json配置文件中设置的绝对路径。net_name:data_dump.json配置文件中设置的网络名称。device_id:训练的卡号。graph_id:训练的图标号。iteration:训练的轮次。operator_name:算子名称。input_output_index :输入或输出标号,例如output_0表示该文件是该算子的第1个输出Tensor的数据。shape: 张量维度信息。data_type: 数据类型。format: 数据格式。5、同步Dump数据分析样例对于Ascend场景,在通过Dump功能将脚本对应的图保存到磁盘上后,会产生最终执行图文件ms_output_trace_code_graph_{graph_id}.ir。该文件中保存了对应的图中每个算子的堆栈信息。若用户想查看Resnet50脚本(同异步Dump数据分析样例)中第一个卷积算子的权重:x = self.conv1(x)(1) 查找算子对应的数据文件与异步Dump数据分析样例中查找方式相同。搜索到相应的文件名:Default--network-TrainOneStepCell--network-WithLossCell--_backbone-ResNet--conv1-Conv2d--Conv2D-op516_input_1_shape_64_3_3_3_Float32_DefaultFormat.bin文件名中可以得到如下信息:shape: 张量维度是(64,3,3,3);data_type: 数据类型为Float32;通过numpy.fromfile接口,还原数据:(2)在终端中依次执行:python->import numpy->array = numpy.fromfile("Default--network-TrainOneStepCell--network-WithLossCell--_backbone-ResNet--conv1-Conv2d--Conv2D- op516_input_1_shape_64_3_3_3_Float32_DefaultFormat.bin", numpy.float32)->numpy.reshape(array, (64,3,3,3))还原原始shape数据:array([[[[-0.01689148, 0.05319214, 0.00757217], [ 0.02868652, 0.00693512, 0.08831787], [ 0.01548767, 0.20080566, 0.22070312]], ... [[ 0.09954834, 0.07037354, 0.15905762], [-0.14477539, -0.11743164, 0.11804199], [-0.16235352, -0.13134766, -0.13427734]]]], dtype=float32)
推荐直播
-
华为AI技术发展与挑战:集成需求分析的实战指南
2024/11/26 周二 18:20-20:20
Alex 华为云学堂技术讲师
本期直播将综合讨论华为AI技术的发展现状,技术挑战,并深入探讨华为AI应用开发过程中的需求分析过程,从理论到实践帮助开发者快速掌握华为AI应用集成需求的框架和方法。
去报名 -
华为云DataArts+DWS助力企业数据治理一站式解决方案及应用实践
2024/11/27 周三 16:30-18:00
Walter.chi 华为云数据治理DTSE技术布道师
想知道数据治理项目中,数据主题域如何合理划分?数据标准及主数据标准如何制定?数仓分层模型如何合理规划?华为云DataArts+DWS助力企业数据治理项目一站式解决方案和应用实践告诉您答案!本期将从数据趋势、数据治理方案、数据治理规划及落地,案例分享四个方面来助力企业数据治理项目合理咨询规划及顺利实施。
去报名
热门标签