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