1# TS&JS高性能编程实践及使用工具的指导 2 3## 概述 4 5本文参考业界标准,并结合应用TS&JS部分的性能优化实践经验,从应用编程指南、高性能编程实践、性能优化调试工具等维度,为应用开发者提供参考指导,助力开发者开发出高性能的应用。 6 7本文主要提供TS&JS高性能编程实践及如何使用性能优化工具的相关建议。 8 9## 应用TS&JS高性能编程实践 10 11高性能编程实践,是在开发过程中逐步总结出来的一些高性能的写法和建议,在业务功能实现过程中,我们要同步思考并理解高性能写法的原理,运用到代码逻辑实现中。 12 13本文中的实践示例代码,会统一标注正例或者反例,正例为推荐写法,反例为不推荐写法。 14 15### 属性访问与属性增删 16 17#### 热点循环中常量提取,减少属性访问次数 18 19在实际的应用场景中抽离出来如下用例,其在循环中会大量进行一些常量的访问操作,该常量在循环中不会改变,可以提取到循环外部,减少属性访问的次数。 20 21【反例】 22 23``` TypeScript 24// 优化前代码 25class Time { 26 static START: number = 1987; 27 static INFO: number[] = [2001, 2002, 2003, 2004, 2005, 2006] 28} 29 30function getDay(year: number): number { 31 let totalDays: number = 348; 32 for (let index: number = 0x8000; index > 0x8; index >>= 1) { 33 // 此处会多次对Time的INFO及START进行查找,并且每次查找出来的值是相同的 34 totalDays += ((Time.INFO[year - Time.START] & index) !== 0) ? 1 : 0; 35 } 36 return totalDays; 37} 38``` 39 40可以将`Time.INFO[year - Time.START]`进行热点函数常量提取操作,这样可以大幅减少属性的访问次数,性能收益明显。 41 42【正例】 43 44``` TypeScript 45// 优化后代码 46class Time { 47 static START: number = 1987; 48 static INFO: number[] = [2001, 2002, 2003, 2004, 2005, 2006]; 49} 50 51function getDay(year: number): number { 52 let totalDays: number = 348; 53 // 从循环中提取不变量 54 const info = Time.INFO[year - Time.START]; 55 for (let index: number = 0x8000; index > 0x8; index >>= 1) { 56 if ((info & index) !== 0) { 57 totalDays++; 58 } 59 } 60 return totalDays; 61} 62``` 63 64#### 避免频繁使用delete 65 66delete对象的某一个属性会改变其布局,影响运行时优化效果,导致执行性能下降。 67 68> **说明:** 69> 70> 不建议直接使用delete删除对象的任何属性,如果有需要,建议使用map和set或者引擎实现的[高性能容器类](../arkts-utils/container-overview.md)。 71 72【反例】 73 74``` TypeScript 75class O1 { 76 x: string | undefined = ""; 77 y: string | undefined = ""; 78} 79 80let obj: O1 = { x: "", y: "" }; 81 82obj.x = "xxx"; 83obj.y = "yyy"; 84delete obj.x; 85``` 86 87建议使用如下两种写法之一实现属性的增删。 88 89【正例】 90 91``` TypeScript 92// 例1:将Object中不再使用的属性设置为null 93class O1 { 94 x: string | null = ""; 95 y: string | null = ""; 96} 97 98let obj: O1 = { x: "", y: "" }; 99 100obj.x = "xxx"; 101obj.y = "yyy"; 102obj.x = null; 103 104// 例2:使用高性能容器类操作属性 105import HashMap from '@ohos.util.HashMap'; 106 107let myMap = new HashMap<string, string>(); 108 109myMap.set("x", "xxx"); 110myMap.set("y", "yyy"); 111myMap.remove("x"); 112``` 113 114### 数值计算 115 116#### 数值计算避免溢出 117 118常见的可能导致溢出的数值计算包括如下场景,溢出之后,会导致引擎走入慢速的溢出逻辑分支处理,影响后续的性能。 119 120- 针对加法、减法、乘法、指数运算等运算操作,应避免数值大于INT32_MAX或小于INT32_MIN,否则会导致int溢出。 121 122- 针对&(and)、>>>(无符号右移)等运算操作,应避免数值大于INT32_MAX,否则会导致int溢出。 123 124### 数据结构 125 126#### 使用合适的数据结构 127 128在实际的应用场景中抽离出来如下用例,该接口中使用JS Object来作为容器去处理Map的逻辑,建议使用HashMap来进行处理。 129 130【反例】 131 132``` TypeScript 133class InfoUtil { 134 getInfo(t1: string, t2: string): string { 135 if (t1 === t2) { 136 return ""; 137 } 138 // 此处使用Record普通对象作为容器 139 let info: Record<string, string> = {}; 140 this.setInfo(info); 141 let t3 = info[t2]; 142 return (t3 != null) ? t3 : ""; 143 } 144 145 setInfo(info: Record<string, string>) { 146 // 接口内部实际上进行的是map的操作 147 info.aaa = 'aaa'; 148 info.bbb = 'bbb'; 149 info.ccc = 'ccc'; 150 } 151} 152``` 153 154代码可以进行如下修改,除了使用引擎中提供的标准内置map之外,还可以使用ArkTS提供的[高性能容器类](../arkts-utils/container-overview.md)。 155 156【正例】 157 158``` TypeScript 159import HashMap from '@ohos.util.HashMap'; 160 161class InfoUtil { 162 getInfo(t1: string, t2: string): string { 163 if (t1 === t2) { 164 return ""; 165 } 166 // 此处改为使用HashMap进行读写操作 167 let info: HashMap<string, string> = new HashMap(); 168 this.setInfo(info); 169 let t3 = info.get(t2); 170 return (t3 != null) ? t3 : ""; 171 } 172 setInfo(info:HashMap<string, string>) { 173 // ... 174 info.set('aaa','aaa'); 175 info.set('bbb','bbb'); 176 info.set('ccc','ccc'); 177 } 178} 179``` 180 181#### 数值数组推荐使用TypedArray 182 183如果是涉及纯数值计算的场合,推荐使用TypedArray数据结构。 184 185常见的TypedArray包括:Int8Array、Uint8Array、Uint8ClampedArray、Int16Array、Uint16Array、Int32Array、Uint32Array、Float32Array、Float64Array、BigInt64Array、BigUint64Array。 186 187【正例】 188 189``` TypeScript 190const typedArray1 = new Int8Array([1, 2, 3]); // 针对这一场景,建议不要使用new Array([1, 2, 3]) 191const typedArray2 = new Int8Array([4, 5, 6]); // 针对这一场景,建议不要使用new Array([4, 5, 6]) 192let res = new Int8Array(3); 193for (let i = 0; i < 3; i++) { 194 res[i] = typedArray1[i] + typedArray2[i]; 195} 196``` 197 198#### 避免使用稀疏数组 199 200分配数组时,应避免其大小超过1024或形成稀疏数组。 201 202虚拟机在分配超过1024大小的数组或者针对稀疏数组,均采用hash表来存储元素,相对使用偏移来访问数组元素速度较慢。 203 204在开发时,尽量避免数组变成稀疏数组。 205 206【反例】 207 208``` TypeScript 209// 如下几种情形会变成稀疏数组 210// 1. 直接分配100000大小的数组,虚拟机会处理成用hash表来存储元素 211let count = 100000; 212let result: number[] = new Array(count); 213 214// 2. 分配数组之后直接,在9999处初始化,会变成稀疏数组 215let result: number[] = new Array(); 216result[9999] = 0; 217 218// 3. 删除数组的element属性,虚拟机也会处理成用hash表来存储元素 219let result = [0, 1, 2, 3, 4]; 220delete result[0]; 221``` 222 223### 对象初始化 224 225#### 对象构造初始化 226 227对象构造的时候,要提供默认值初始化,不要访问未初始化的属性。 228 229【反例】 230 231``` TypeScript 232// 不要访问未初始化的属性 233class A { 234 x: number; 235} 236 237// 构造函数中要对属性进行初始化 238class A { 239 x: number; 240 constructor() { 241 } 242} 243 244let a = new A(); 245// x使用时还未赋值,这种情况会访问整个原型链 246console.log((a.x).toString()); 247``` 248 249【正例】 250 251``` TypeScript 252// 推荐一:声明初始化 253class A { 254 x: number = 0; 255} 256 257// 推荐二:构造函数直接赋初值 258class A { 259 constructor() { 260 this.x = 0; 261 } 262} 263 264let a = new A(); 265console.log((a.x).toString()); 266``` 267 268#### number正确初始化 269 270针对number类型,编译器在优化时会区分整型和浮点类型。开发者在初始化时如果预期是整型就初始化成0,如果预期是浮点型就初始化为0.0,不要把一个number类型初始化成undefined或者null。 271 272【正例】 273 274``` TypeScript 275function foo(d: number) : number { 276 // 变量i预期是整型,不要声明成undefined/null或0.0,直接初始化为0 277 let i: number = 0; 278 i += d; 279 return i; 280} 281``` 282 283#### 避免动态添加属性 284 285对象在创建的时候,如果开发者明确后续还需要添加属性,可以提前置为undefined。动态添加属性会导致对象布局变化,影响编译器和运行时优化效果。 286 287【反例】 288 289``` TypeScript 290// 后续obj需要再添加z属性 291class O1 { 292 x: string = ""; 293 y: string = ""; 294} 295let obj: O1 = {"x": "xxx", "y": "yyy"}; 296// ... 297// 这种动态添加方式是不推荐的 298obj.z = "zzz"; 299``` 300 301【正例】 302 303``` TypeScript 304class O1 { 305 x: string = ""; 306 y: string = ""; 307 z: string = ""; 308} 309let obj: O1 = {"x": "xxx", "y": "yyy", "z": ""}; 310// ... 311obj.z = "zzz"; 312``` 313 314#### 调用构造函数的入参要与标注类型匹配 315 316由于TS语言类型系统是一种标注类型,不是编译期强制约束,如果入参的实际类型与标注类型不匹配,会影响引擎内部的优化效果。 317 318【反例】 319 320``` TypeScript 321class A { 322 private a: number | undefined; 323 private b: number | undefined; 324 private c: number | undefined; 325 constructor(a?: number, b?: number, c?: number) { 326 this.a = a; 327 this.b = b; 328 this.c = c; 329 } 330} 331// new的过程中没有传入参数,a,b,c会获取一个undefined的初值,和标注类型不符 332let a = new A(); 333``` 334 335针对上文的示例场景,开发者大概率预期该入参类型是number类型,需要显式写出来。 336 337参照正例进行如下修改,不然会造成标注的入参是number,实际传入的是undefined。 338 339【正例】 340 341``` TypeScript 342class A { 343 private a: number | undefined; 344 private b: number | undefined; 345 private c: number | undefined; 346 constructor(a?: number, b?: number, c?: number) { 347 this.a = a; 348 this.b = b; 349 this.c = c; 350 } 351} 352// 初始化直接传入默认值0 353let a = new A(0, 0, 0); 354``` 355 356#### 不变的变量声明为const 357 358不变的变量推荐使用const进行初始化。 359 360【反例】 361 362``` TypeScript 363// 该变量在后续过程中并未发生更改,建议声明为常量 364let N = 10000; 365 366function getN() { 367 return N; 368} 369``` 370 371【正例】 372 373``` TypeScript 374const N = 10000; 375 376function getN() { 377 return N; 378} 379``` 380 381### 接口及继承 382 383#### 避免使用type类型标注 384 385如果传入的参数类型是type类型,实际入参可能是一个object literal,也可能是一个class,编译器及虚拟机因为类型不固定,无法做编译期假设进而进行相应的优化。 386 387【反例】 388 389``` TypeScript 390// type类型无法在编译期确认, 可能是一个object literal,也可能是另一个class Person 391type Person = { 392 name: string; 393 age: number; 394}; 395 396function greet(person: Person) { 397 return "Hello " + person.name; 398} 399 400// type方式是不推荐的,因为其有如下两种使用方式,type类型无法在编译期确认 401// 调用方式一 402class O1 { 403 name: string = ""; 404 age: number = 0; 405} 406let objectliteral: O1 = {name : "zhangsan", age: 20 }; 407greet(objectliteral); 408 409// 调用方式二 410class Person { 411 name: string = "zhangsan"; 412 age: number = 20; 413} 414let person = new Person(); 415greet(person); 416``` 417 418【正例】 419 420``` TypeScript 421interface Person { 422 name: string ; 423 age: number; 424} 425 426function greet(person: Person) { 427 return "Hello " + person.name; 428} 429 430class Person { 431 name: string = "zhangsan"; 432 age: number = 20; 433} 434 435let person = new Person(); 436greet(person); 437``` 438 439### 函数调用 440 441#### 声明参数要和实际的参数一致 442 443声明的参数要和实际的传入参数个数及类型一致,如果不传入参数,则会作为undefined处理,可能造成与实际入参类型不匹配的情况,从而导致运行时走入慢速路径,影响性能。 444 445【反例】 446 447``` TypeScript 448function add(a: number, b: number) { 449 return a + b; 450} 451// 参数个数是2,不能给3个 452add(1, 2, 3); 453// 参数个数是2,不能给1个 454add(1); 455// 参数类型是number,不能给string 456add("hello", "world"); 457``` 458 459【正例】 460 461``` TypeScript 462function add(a: number, b: number) { 463 return a + b; 464} 465// 按照函数参数个数及类型要求传入参数 466add(1, 2); 467``` 468 469#### 函数内部变量尽量使用参数传递 470 471能传递参数的尽量传递参数,不要使用闭包。闭包作为参数会多一次闭包创建和访问。 472 473【反例】 474 475``` TypeScript 476let arr = [0, 1, 2]; 477 478function foo() { 479 // arr 尽量通过参数传递 480 return arr[0] + arr[1]; 481} 482foo(); 483``` 484 485【正例】 486 487``` TypeScript 488let arr = [0, 1, 2]; 489 490function foo(array: Array) : number { 491 // arr 尽量通过参数传递 492 return array[0] + array[1]; 493} 494foo(arr); 495``` 496 497### 函数与类声明 498 499#### 避免动态声明function与class 500 501不建议动态声明function和class。 502 503以如下用例为例,动态声明了class Add和class Sub,每次调用`foo`都会重新创建class Add和class Sub,对内存和性能都会有影响。 504 505【反例】 506 507``` TypeScript 508function foo(f: boolean) { 509 if (f) { 510 return class Add{}; 511 } else { 512 return class Sub{}; 513 } 514} 515``` 516 517【正例】 518 519``` TypeScript 520class Add{}; 521class Sub{}; 522function foo(f: boolean) { 523 if (f) { 524 return Add; 525 } else { 526 return Sub; 527 } 528} 529``` 530 531## TS&JS性能优化工具使用 532 533通过如下工具和使用方法,能够帮助开发者查看待分析场景下各阶段的耗时分布情况,并进一步针对耗时情况使用对应的工具做细化分析。 534 535工具使用介绍: 536 5371. 针对应用开发者,推荐使用自带的[Smartperf工具](../../device-dev/device-test/smartperf-host.md)来进行辅助分析,可以从宏观角度查看应用各个阶段耗时分布情况,快速找到待分析优化模块。 5382. 针对第一步分析得到的待优化模块,需要进行进一步分析确认耗时点是在TS&JS部分还是C++部分。C++部分耗时模块细化分析建议使用hiperf工具;针对TS&JS部分耗时,可以使用[CPU Profiler工具](application-performance-analysis.md)。 5393. 针对虚拟机开发者,如果需要进一步拆分细化,推荐使用虚拟机提供的RUNTIME_STAT工具。 540 541### Smartperf工具使用指导 542 543以如下某个应用场景使用过程的trace为例,可以通过[Smartperf工具](../../device-dev/device-test/smartperf-host.md)抓取到应用使用阶段的耗时信息,其中大部分为GC(Garbage Collection,垃圾回收)等操作。如果此接口大部分是应用开发者通过TS&JS实现,并且在trace中体现此阶段比较耗时,则可以继续使用[CPU Profiler工具](application-performance-analysis.md)来进一步分析TS&JS部分耗时情况。 544 545除了可以查看系统的trace之外,还可以在应用的源码的关键流程中加入一些trace点,用于做性能分析。startTrace用于记录trace起点,finishTrace用于记录trace终点,在应用中增加trace点的方式如下: 546 547``` TypeScript 548import hiTraceMeter from '@ohos.hiTraceMeter'; 549// ... 550hiTraceMeter.startTrace("fillText1", 100); 551// ... 552hiTraceMeter.finishTrace("fillText1", 100); 553``` 554 555### hiperf工具使用指导 556 557集成在Smartperf的hiperf工具使用指导,具体可见 [HiPerf的抓取和展示说明](https://gitee.com/openharmony/developtools_smartperf_host/blob/master/ide/src/doc/md/quickstart_hiperf.md)。 558 559hiperf工具的单独使用指导,具体可见 [hiperf应用性能优化工具](https://gitee.com/openharmony/developtools_hiperf)。 560 561### TS&JS及NAPI层面耗时分析工具 562 563TS&JS层面耗时主要分为如下几种情况: 564 5651. Ability的生命周期回调的耗时。 566 5672. 组件的TS&JS业务代码的回调的耗时。 568 5693. 应用TS&JS逻辑代码耗时。 570 571NAPI层面的耗时主要分为如下几种情况: 572 5731. TS&JS业务代码通过调用JS API产生的耗时。 574 5752. TS&JS业务代码调用开发者通过NAPI封装的C/C++实现时产生的耗时。 576 577针对应用中的TS&JS及NAPI两种业务场景的耗时分析,我们提供了[CPU Profiler工具](application-performance-analysis.md),用来识别热点函数及耗时代码。 578 579其支持的采集方式如下: 580 581- DevEco Studio连接设备实时采集; 582 583- hdc shell连接设备进行命令行采集。 584 585可以通过CPU Profiler工具,对TS&JS中执行的热点函数进行抓取。以应用实际使用场景为例,在此场景中,可以抓到应用中的某一热点函数,在此基础上,针对该接口做进一步分析。 586