[譯]Jake Wharton 提問:除以 2 ,右移 1,誰更好 ?

原文做者: Jake Whartonjava

原文標題:Which is better on Android: divide by 2 or shift by 1?android

原文地址:https://jakewharton.com/which-is-better-on-android-divide-by-two-or-shift-by-one/web

譯者:秉心說shell

我一直在嘗試將 AndroidX collection library 移植到 Kotlin multiplatform,來測試二進制兼容性,性能,易用性和不一樣的內存模型。類庫中的一些數據結構使用基於數組實現的二叉樹來存儲元素。在 Java 代碼中有許多地方使用 移位操做 來代替二次冪的乘除法。當移植到 Kotlin 時,這些代碼會被轉化爲略顯變扭的中綴操做符,這有點混淆了代碼意圖。api

關於移位運算和乘/除法誰的性能更好,我作過一些調研,大多數人都據說過 「移位運算性能更好」,但也對其真實性保持質疑。一些人認爲代碼運行到 CPU 以前,編譯器可能會作一些優化。數組

爲了知足個人好奇心,同時避免使用 Kotlin 的中綴移位操做符,我會來回答誰更好以及一些相關問題。Let's go !安全

譯者注:Jake Wharton 吐槽的 Kotlin 移位操做是這麼寫的:i shr 1i shl 1數據結構

誰優化了代碼 ?

在咱們的代碼被 CPU 執行以前,有如下幾個重要的編譯器:javac/kotlincD八、R8ARTjvm

其中的每一步都有機會作優化,可是它們作了嗎?編輯器

javac

class Example {
  static int multiply(int value) {
    return value * 2;
  }
  static int divide(int value) {
    return value / 2;
  }
  static int shiftLeft(int value) {
    return value << 1;
  }
  static int shiftRight(int value) {
    return value >> 1;
  }
}
複製代碼

在 JDK 14 下編譯上面的代碼,並經過 javap 展現字節碼。

$ javac Example.java
$ javap -c Example
Compiled from "Example.java"
class Example {
  static int multiply(int);
    Code:
       0: iload_0
       1: iconst_2
       2: imul
       3: ireturn  static int divide(int); Code: 0: iload_0 1: iconst_2 2: idiv 3: ireturn  static int shiftLeft(int); Code: 0: iload_0 1: iconst_1 2: ishl 3: ireturn  static int shiftRight(int); Code: 0: iload_0 1: iconst_1 2: ishr 3: ireturn } 複製代碼

每一個方法都以 iload_0 指令開頭,表示加載第一個參數。乘法和除法都是用 iconst_2 指令來加載字面量 2 。而後分別執行 imulidiv 指令來進行 int 類型的乘除法。移位操做也是先加載字面量 1,而後利用 ishlishr 指令進行移位運算。

這裏沒有進行任何優化,可是若是你對 java 有所瞭解的話,也不會感到意外。javac 並非一個會進行優化的編譯器,而是把大部分工做留給了 JVM 上的運行時編譯器或者 AOT 。

kotlinc

fun multiply(value: Int) = value * 2
fun divide(value: Int) = value / 2
fun shiftLeft(value: Int) = value shl 1
fun shiftRight(value: Int) = value shr 1
複製代碼

在 Kotlin 1.4-M1 版本下經過 kotlinc 將 Kotlin 編譯成 Java 字節碼,再使用 javap 查看。

$ kotlinc Example.kt
$ javap -c ExampleKt
Compiled from "Example.kt"
public final class ExampleKt {
  public static final int multiply(int);
    Code:
       0: iload_0
       1: iconst_2
       2: imul
       3: ireturn  public static final int divide(int); Code: 0: iload_0 1: iconst_2 2: idiv 3: ireturn  public static final int shiftLeft(int); Code: 0: iload_0 1: iconst_1 2: ishl 3: ireturn  public static final int shiftRight(int); Code: 0: iload_0 1: iconst_1 2: ishr 3: ireturn } 複製代碼

輸出結果和 Java 徹底一致。

This is using the original JVM backend of Kotlin, but using the forthcoming IR-based backend (via -Xuse-ir) also produces the same output.

上面這句裱起來,由於我看不懂 ~

D8

使用最新的 D8 編譯器將上面示例的 Kotlin 代碼轉換的字節碼生成 DEX 文件。

$ java -jar $R8_HOME/build/libs/d8.jar \ --release \ --output . \ ExampleKt.class $ dexdump -d classes.dex Opened 'classes.dex', DEX version '035' Class #0 - Class descriptor : 'LExampleKt;' Access flags : 0x0011 (PUBLIC FINAL) Superclass : 'Ljava/lang/Object;' Direct methods - #0 : (in LExampleKt;) name : 'divide' type : '(I)I' access : 0x0019 (PUBLIC STATIC FINAL) code - 000118: |[000118] ExampleKt.divide:(I)I 000128: db00 0102 |0000: div-int/lit8 v0, v1, #int 2 // #02 00012c: 0f00 |0002: return v0
#1              : (in LExampleKt;)
  name          : 'multiply'
  type          : '(I)I'
  access        : 0x0019 (PUBLIC STATIC FINAL)
  code          -
複製代碼

000130: |[000130] ExampleKt.multiply:(I)I 000140: da00 0102 |0000: mul-int/lit8 v0, v1, #int 2 // #02 000144: 0f00 |0002: return v0

#2              : (in LExampleKt;)
  name          : 'shiftLeft'
  type          : '(I)I'
  access        : 0x0019 (PUBLIC STATIC FINAL)
  code          -
複製代碼

000148: |[000148] ExampleKt.shiftLeft:(I)I 000158: e000 0101 |0000: shl-int/lit8 v0, v1, #int 1 // #01 00015c: 0f00 |0002: return v0

#3              : (in LExampleKt;)
  name          : 'shiftRight'
  type          : '(I)I'
  access        : 0x0019 (PUBLIC STATIC FINAL)
  code          -
複製代碼
複製代碼#1 : (in LExampleKt;) name : 'multiply' type : '(I)I' access : 0x0019 (PUBLIC STATIC FINAL) code - 複製代碼#2 : (in LExampleKt;) name : 'shiftLeft' type : '(I)I' access : 0x0019 (PUBLIC STATIC FINAL) code - 複製代碼#3 : (in LExampleKt;) name : 'shiftRight' type : '(I)I' access : 0x0019 (PUBLIC STATIC FINAL) code - 複製代碼000160: |[000160] ExampleKt.shiftRight:(I)I 000170: e100 0101 |0000: shr-int/lit8 v0, v1, #int 1 // #01 000174: 0f00 |0002: return v0 複製代碼

(略微優化了輸出結果)

Dalvik 字節碼是基於寄存器的,Java 字節碼是基於棧的。最終,每一個方法實際上都僅僅使用了一個字節碼來操做相關聯的整型數運算。它們都使用了 v1 寄存器來保存第一個方法參數,另外還須要一個字面量 1 或者 2。

因此不會產生任何改變,D8 並非一個優化編譯器(儘管它能夠作 method-local optimization) 。

R8

爲了運行 R8,咱們須要配置混淆規則防止咱們的代碼被移除。

-keep,allowoptimization class ExampleKt {
  <methods>;
}
複製代碼

上面的規則經過 --pg-conf 參數傳遞。

$ java -jar $R8_HOME/build/libs/r8.jar \
      --lib $ANDROID_HOME/platforms/android-29/android.jar \
      --release \
      --pg-conf rules.txt \
      --output . \
      ExampleKt.class
$ dexdump -d classes.dex
Opened 'classes.dex', DEX version '035'
Class #0            -
  Class descriptor  : 'LExampleKt;'
  Access flags      : 0x0011 (PUBLIC FINAL)
  Superclass        : 'Ljava/lang/Object;'
  Direct methods    -
    #0              : (in LExampleKt;)
      name          : 'divide'
      type          : '(I)I'
      access        : 0x0019 (PUBLIC STATIC FINAL)
      code          -
000118:                              |[000118] ExampleKt.divide:(I)I
000128: db00 0102                    |0000: div-int/lit8 v0, v1, #int 2 // #02
00012c: 0f00                         |0002: return v0
 #1 : (in LExampleKt;) name : 'multiply' type : '(I)I' access : 0x0019 (PUBLIC STATIC FINAL) code - 000130: |[000130] ExampleKt.multiply:(I)I 000140: da00 0102 |0000: mul-int/lit8 v0, v1, #int 2 // #02 000144: 0f00 |0002: return v0  #2 : (in LExampleKt;) name : 'shiftLeft' type : '(I)I' access : 0x0019 (PUBLIC STATIC FINAL) code - 000148: |[000148] ExampleKt.shiftLeft:(I)I 000158: e000 0101 |0000: shl-int/lit8 v0, v1, #int 1 // #01 00015c: 0f00 |0002: return v0  #3 : (in LExampleKt;) name : 'shiftRight' type : '(I)I' access : 0x0019 (PUBLIC STATIC FINAL) code - 000160: |[000160] ExampleKt.shiftRight:(I)I 000170: e100 0101 |0000: shr-int/lit8 v0, v1, #int 1 // #01 000174: 0f00 |0002: return v0 複製代碼

和 D8 的輸出如出一轍。

ART

使用上面 R8 輸出的 Dalvik 字節碼做爲 ART 的輸入,在 Android 10 的 x86 虛擬機上運行。

$ adb push classes.dex /sdcard/classes.dex
$ adb shell
generic_x86:/ $ su
generic_x86:/ # dex2oat --dex-file=/sdcard/classes.dex --oat-file=/sdcard/classes.oat
generic_x86:/ # oatdump --oat-file=/sdcard/classes.oat
OatDexFile:
0: LExampleKt; (offset=0x000003c0) (type_idx=1) (Initialized) (OatClassAllCompiled)
  0: int ExampleKt.divide(int) (dex_method_idx=0)
    CODE: (code_offset=0x00001010 size_offset=0x0000100c size=15)...
      0x00001010:     89C8      mov eax, ecx
      0x00001012:   8D5001      lea edx, [eax + 1]
      0x00001015:     85C0      test eax, eax
      0x00001017:   0F4DD0      cmovnl/ge edx, eax
      0x0000101a:     D1FA      sar edx
      0x0000101c:     89D0      mov eax, edx
      0x0000101e:       C3      ret
  1: int ExampleKt.multiply(int) (dex_method_idx=1)
    CODE: (code_offset=0x00001030 size_offset=0x0000102c size=5)...
      0x00001030:     D1E1      shl ecx
      0x00001032:     89C8      mov eax, ecx
      0x00001034:       C3      ret
  2: int ExampleKt.shiftLeft(int) (dex_method_idx=2)
    CODE: (code_offset=0x00001030 size_offset=0x0000102c size=5)...
      0x00001030:     D1E1      shl ecx
      0x00001032:     89C8      mov eax, ecx
      0x00001034:       C3      ret
  3: int ExampleKt.shiftRight(int) (dex_method_idx=3)
    CODE: (code_offset=0x00001040 size_offset=0x0000103c size=5)...
      0x00001040:     D1F9      sar ecx
      0x00001042:     89C8      mov eax, ecx
      0x00001044:       C3      ret
複製代碼

(略微優化了輸出結果)

x86 的彙編代碼代表 ART 介入了數學運算,並使用移位操做代替了其中的一部分。

首先,multiplyshiftLeft 如今具備一樣的實現,它們都使用 shl 來進行左移一位的操做。除此以外,若是你查看文件偏移量(最左邊一列)的話,你會發現是徹底同樣的。ART 識別到了這兩個方法具備同樣的方法體,並在編譯成 x86 彙編代碼時進行了去重操做。

而後,divideshiftRight 的實現是不同的,它們沒有共同使用 sar 來進行右移一位的操做。divide 方法在調用 sar 以前額外使用了四條指令,來處理輸入是負數的狀況。

在 Android 10 Pixel4 的設備上執行相同的指令,來看看 ART 是如何將代碼編譯成 ARM 彙編代碼的。

OatDexFile:
0: LExampleKt; (offset=0x000005a4) (type_idx=1) (Verified) (OatClassAllCompiled)
  0: int ExampleKt.divide(int) (dex_mmultiply and shiftLeft ethod_idx=0)
    CODE: (code_offset=0x00001009 size_offset=0x00001004 size=10)...
      0x00001008: 0fc8      lsrs r0, r1, #31
      0x0000100a: 1841      adds r1, r0, r1
      0x0000100c: 1049      asrs r1, #1
      0x0000100e: 4608      mov r0, r1
      0x00001010: 4770      bx lr
  1: int ExampleKt.multiply(int) (dex_method_idx=1)
    CODE: (code_offset=0x00001021 size_offset=0x0000101c size=4)...
      0x00001020: 0048      lsls r0, r1, #1
      0x00001022: 4770      bx lr
  2: int ExampleKt.shiftLeft(int) (dex_method_idx=2)
    CODE: (code_offset=0x00001021 size_offset=0x0000101c size=4)...
      0x00001020: 0048      lsls r0, r1, #1
      0x00001022: 4770      bx lr
  3: int ExampleKt.shiftRight(int) (dex_method_idx=3)
    CODE: (code_offset=0x00001031 size_offset=0x0000102c size=4)...
      0x00001030: 1048      asrs r0, r1, #1
      0x00001032: 4770      bx lr
複製代碼

一樣的,multiplyshiftLeft 使用 lsls 來完成左移一位的操做並去除了重複方法體。shiftRight 經過 asrs 指令完成右移,而除法使用了另外一個右移指令 lsrs來處理輸入是負數的狀況。

到此爲止,咱們能夠確定的說,使用 value << 1 來代替 value * 2 不會帶來任何好處。 中止在算數運算中作這樣的事情吧,僅在嚴格要求按位運算的狀況下保留。

可是,value / 2value >> 1 仍然會產生不一樣的彙編指令,所以也會有不同的性能表現。值得慶幸的是,value / 2 並不會進行通用的除法運算,仍然是基於移位操做,所以它們的性能差別可能並不大。

移位比除法快嗎 ?

爲了肯定移位操做和除法運算誰更快,我使用了 Jetpack benchmark 進行了測試。

class DivideOrShiftTest {
  @JvmField @Rule val benchmark = BenchmarkRule()
 @Test fun divide() { val value = "4".toInt() // Ensure not a constant. var result = 0 benchmark.measureRepeated { result = value / 2 } println(result) // Ensure D8 keeps computation. }  @Test fun shift() { val value = "4".toInt() // Ensure not a constant. var result = 0 benchmark.measureRepeated { result = value shr 1 } println(result) // Ensure D8 keeps computation. } } 複製代碼

我沒有 x86 設備,因此我在 Android 10 Pixel3 上進行了測試,結果以下:

android.studio.display.benchmark=4 ns DivideOrShiftTest.divide count=4006 mean=4 median=4 min=4 standardDeviation=0 複製代碼android.studio.display.benchmark=3 ns DivideOrShiftTest.shift count=3943 mean=3 median=3 min=3 standardDeviation=0 複製代碼

使用除法和移位實際上並無什麼區別,它們的差距是納秒級的。使用負數的話,結果不會有任何差別。

到此爲止,咱們能夠確定的說,使用 value >> 1 來代替 value / 2 不會帶來任何好處。 中止在算數運算中作這樣的事情吧,僅在嚴格要求按位運算的狀況下保留。

D8/R8 能夠減少 Apk 體積嗎?

對於同一操做有兩種表達方式的話,應該選擇性能更優的。若是性能相同,就應該選擇能下降 Apk 體積的。

如今咱們都知道了 value * 2value << 1 在 ART 上產生了相同的彙編代碼。所以,若是哪種可以在 Dalvik 上更加節省空間,咱們就應該毫無疑問的使用它來代替另外一種寫法。讓咱們來看看 D8 的輸出,它也產生了相同大小的字節碼:

#1 : (in LExampleKt;) name : 'multiply' ⋮ 000140: da00 0102 |0000: mul-int/lit8 v0, v1, #int 2 // #02
#2              : (in LExampleKt;)
  name          : 'shiftLeft'
  ⋮
複製代碼
複製代碼#2 : (in LExampleKt;) name : 'shiftLeft' ⋮ 複製代碼000158: e000 0101 |0000: shl-int/lit8 v0, v1, #int 1 // #01 複製代碼

乘法有可能會耗費更多的空間用來存儲字面量。比較一下 value * 32_768value << 15

#1 : (in LExampleKt;) name : 'multiply' ⋮ 000128: 1400 0080 0000 |0000: const v0, #float 0.000000 // #00008000 00012e: 9201 0100 |0003: mul-int v1, v1, v0
#2              : (in LExampleKt;)
  name          : 'shiftLeft'
  ⋮
複製代碼
複製代碼#2 : (in LExampleKt;) name : 'shiftLeft' ⋮ 複製代碼00015c: e000 000f |0000: shl-int/lit8 v0, v0, #int 15 // #0f 複製代碼

我在 D8 上提過這個 issue,但我強烈懷疑出現這一狀況的機率爲 0,因此這並不值得。

D8 和 R8 的輸出也代表,對於 Dalvik 來講,value / 2value >> 1 的代價是相同的。

#0 : (in LExampleKt;) name : 'divide' ⋮ 000128: db00 0102 |0000: div-int/lit8 v0, v1, #int 2 // #02
#2              : (in LExampleKt;)
  name          : 'shiftLeft'
  ⋮
複製代碼
複製代碼#2 : (in LExampleKt;) name : 'shiftLeft' ⋮ 複製代碼000158: e000 0101 |0000: shl-int/lit8 v0, v1, #int 1 // #01 複製代碼

當字面量大小達到 32768 時,上面的字節碼大小也會發生變化。因爲負數的緣由,無條件的使用右移來代替 2 次冪的除法並非絕對安全的。咱們能夠在保證非負數的狀況下進行替換。

無符號數的二次冪除法也使用移位嗎?

Java 字節碼並無無符號數,但你可使用有符號數來模擬。Java 提供了靜態方法能夠將有符號數轉化爲無符號數。Kotlin 提供了無符號類型 UInt ,它提供了同樣的功能,但和 Java 不同的是,它獨立抽象爲一個數據類型。能夠想象到的是,二次冪的除法確定能夠用右移操做重寫。

使用 Kotlin 來演示下面兩種狀況。

fun javaLike(value: Int) = Integer.divideUnsigned(value, 2)
fun kotlinLike(value: UInt) = value / 2U
複製代碼

經過 kotlinc 編譯(Kotlin 1.4-M1) :

$ kotlinc Example.kt $ javap -c ExampleKt Compiled from "Example.kt" public final class ExampleKt { public static final int javaLike(int); Code: 0: iload_0 1: iconst_2 2: invokestatic #12 // Method java/lang/Integer.divideUnsigned:(II)I 5: ireturn 複製代碼public static final int kotlinLike-WZ4Q5Ns(int); Code: 0: iload_0 1: istore_1 2: iconst_2 3: istore_2 4: iconst_0 5: istore_3 6: iload_1 7: iload_2 8: invokestatic #20 // Method kotlin/UnsignedKt."uintDivide-J1ME1BU":(II)I 11: ireturn } 複製代碼

Kotlin 沒有識別到這是一個二次冪的除法,它原本能夠用 iushr 移位操做來代替。我向 Jetbrain 也提交過這個 issue

使用 -Xuse-i 也不會帶來任何改變(除了移除了一些 load/store)。可是,面向 Java8 就不同了。

$ kotlinc -jvm-target 1.8 Example.kt $ javap -c ExampleKt Compiled from "Example.kt" public final class ExampleKt { public static final int javaLike(int); Code: 0: iload_0 1: iconst_2 2: invokestatic #12 // Method java/lang/Integer.divideUnsigned:(II)I 5: ireturn 複製代碼public static final int kotlinLike-WZ4Q5Ns(int); Code: 0: iload_0 1: iconst_2 2: invokestatic #12 // Method java/lang/Integer.divideUnsigned:(II)I 5: ireturn } 複製代碼

Integer.divideUnsigned 方法從 Java 8 開始可用。因爲這樣讓兩個函數體徹底相同了,仍是回到舊版原本進行對比。

接下來是 R8。與上面明顯不一樣的是,咱們使用 Kotlin 標準庫做爲輸入,還指定了最低 api ,--min-api 24 。由於 Integer.divideUnsigned 僅在 API 24 及之後可用。

$ java -jar $R8_HOME/build/libs/r8.jar \ --lib $ANDROID_HOME/platforms/android-29/android.jar \ --min-api 24 \ --release \ --pg-conf rules.txt \ --output . \ ExampleKt.class kotlin-stdlib.jar $ dexdump -d classes.dex Opened 'classes.dex', DEX version '039' Class #0 - Class descriptor : 'LExampleKt;' Access flags : 0x0011 (PUBLIC FINAL) Superclass : 'Ljava/lang/Object;' Direct methods - #0 : (in LExampleKt;) name : 'javaLike' type : '(I)I' access : 0x0019 (PUBLIC STATIC FINAL) code - 0000f8: |[0000f8] ExampleKt.javaLike:(I)I 000108: 1220 |0000: const/4 v0, #int 2 // #2 00010a: 7120 0200 0100 |0001: invoke-static {v1, v0}, Ljava/lang/Integer;.divideUnsigned:(II)I // method@0002 000110: 0a01 |0004: move-result v1 000112: 0f01 |0005: return v1
#1              : (in LExampleKt;)
  name          : 'kotlinLike-WZ4Q5Ns'
  type          : '(I)I'
  access        : 0x0019 (PUBLIC STATIC FINAL)
  code          -
複製代碼
複製代碼#1 : (in LExampleKt;) name : 'kotlinLike-WZ4Q5Ns' type : '(I)I' access : 0x0019 (PUBLIC STATIC FINAL) code - 複製代碼000114: |[000114] ExampleKt.kotlinLike-WZ4Q5Ns:(I)I 000124: 8160 |0000: int-to-long v0, v6 000126: 1802 ffff ffff 0000 0000 |0001: const-wide v2, #double 0.000000 // #00000000ffffffff 000130: c020 |0006: and-long/2addr v0, v2 000132: 1226 |0007: const/4 v6, #int 2 // #2 000134: 8164 |0008: int-to-long v4, v6 000136: c042 |0009: and-long/2addr v2, v4 000138: be20 |000a: div-long/2addr v0, v2 00013a: 8406 |000b: long-to-int v6, v0 00013c: 0f06 |000c: return v6 複製代碼

Kotlin 有本身的無符號整數的實現,並直接內聯到了函數體內。它是這樣實現的,將參數和字面量轉化爲 long ,進行 long 的除法,最後轉換爲 int 。When we eventually run them through ART they’re just translated to equivalent x86 so we’re going to leave this function behind. (這句沒太懂) 。這裏已經錯失了優化機會。

對於 Java 版本,R8 也沒有使用移位運算來代替 divideUnsigned 。我已經提交 issue 來持續進行追蹤。

最後的優化機會就是 ART 。

$ adb push classes.dex /sdcard/classes.dex
$ adb shell
generic_x86:/ $ sugenzong
generic_x86:/ # dex2oat --dex-file=/sdcard/classes.dex --oat-file=/sdcard/classes.oat
generic_x86:/ # oatdump --oat-file=/sdcard/classes.oat
OatDexFile:
0: LExampleKt; (offset=0x000003c0) (type_idx=1) (Initialized) (OatClassAllCompiled)
  0: int ExampleKt.javaLike(int) (dex_method_idx=0)
    CODE: (code_offset=0x00001010 size_offset=0x0000100c size=63)...
      0x00001010:         85842400E0FFFF             test eax, [esp + -8192]
        StackMap[0] (native_pc=0x1017, dex_pc=0x0, register_mask=0x0, stack_mask=0b)
      0x00001017:                     55             push ebp
      0x00001018:                 83EC18             sub esp, 24
      0x0000101b:                 890424             mov [esp], eax
      0x0000101e:     6466833D0000000000             cmpw fs:[0x0], 0  ; state_and_flags
      0x00001027:           0F8519000000             jnz/ne +25 (0x00001046)
      0x0000102d:             E800000000             call +0 (0x00001032)
      0x00001032:                     5D             pop ebp
      0x00001033:             BA02000000             mov edx, 2
      0x00001038:           8B85CE0F0000             mov eax, [ebp + 4046]
      0x0000103e:                 FF5018             call [eax + 24]
        StackMap[1] (native_pc=0x1041, dex_pc=0x1, register_mask=0x0, stack_mask=0b)
      0x00001041:                 83C418             add esp, 24
      0x00001044:                     5D             pop ebp
      0x00001045:                     C3             ret
      0x00001046:         64FF15E0020000             call fs:[0x2e0]  ; pTestSuspend
        StackMap[2] (native_pc=0x104d, dex_pc=0x0, register_mask=0x0, stack_mask=0b)
      0x0000104d:                   EBDE             jmp -34 (0x0000102d)
  1: int ExampleKt.kotlinLike-WZ4Q5Ns(int) (dex_method_idx=1)
    CODE: (code_offset=0x00001060 size_offset=0x0000105c size=67)...
      ⋮
複製代碼

ART 並無內聯調用 divideUnsigned,取而代之的是常規的方法調用。我提交了這個 issue 進行跟蹤。

最後

真是一段漫長的旅程,恭喜你已經完成了(或者只是翻到了文章底部)。讓咱們總結一下。

  1. ART 使用左移/右移重寫了二次冪的乘法/除法(處理負數時會有增長額外的指令)。
  2. 右移和二次冪除法之間並無明顯的性能差距。
  3. 移位和乘除法的 Dalvik 字節碼大小是同樣的。
  4. 沒有人優化了無符號除法(至少如今沒有),可是你應該也沒有用過。

經過這些事實,你能夠回答文章開頭的問題了。

在 Android 上,選擇 除以2 仍是 右移1 ?

都不是!僅在實際須要按位操做時使用移位運算,其餘數學運算使用乘除法。我將着手將 AndroidX collection 的位運算切換到乘除法。下次見!


最近可能譯文會比較多,遇到一些好的文章老是忍不住要分享給你們。

其實譯文並不比原創文輕鬆,我至少都是在通讀一遍,精讀兩遍的基礎下,纔會下筆寫譯文。若是以爲文章不錯,盡情的在看,轉發,分享吧!

本文使用 mdnice 排版

相關文章
相關標籤/搜索