0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

為什么在JVM x86平臺生成的機器代碼中會看到XMM寄存器?

冬至子 ? 來源:ImportNew ? 作者:ImportNew ? 2023-06-15 15:22 ? 次閱讀

2. 問題

代碼中沒有浮點或矢量操作,為什么在 JVM x86 平臺生成的機器代碼中會看到 XMM 寄存器?

3. 理論

FPU 和矢量單元在現(xiàn)代 CPU 中無處不在。通常,它們會為 FPU 特定操作提供了備用寄存器。例如,英特爾 x86_64 平臺的 SSE 和 AVX 擴展包含了一組豐富的 XMM、YMM 和 ZMM 寄存器供指令操作。

雖然非矢量指令集與矢量、非矢量寄存器通常不會正交,比如不能在 x86_64 上對 XMM 寄存器執(zhí)行通用 IMUL,但是這些寄存器仍然提供了一種存儲選項。即使不用于矢量計算,也可以在這些寄存器中存儲數(shù)據(jù)。

(1) 最極端的情況是把矢量寄存器當緩沖用。

寄存器分配器的任務(wù)是在一個特定的編譯單元(比如方法)中獲取程序需要的所有操作數(shù),并為它們分配寄存器——映射到機器實際寄存器。真實程序中,需要的操作數(shù)大于機器中可用的寄存器數(shù)目。這時寄存器分配器必須把一些操作數(shù)放到寄存器以外的某個地方(比如堆棧),也就是說會發(fā)生操作數(shù)溢出。

x86_64 上有16個通用寄存器(并非每個寄存器都可用)。目前,大多數(shù)機器還有16個 AVX 寄存器。發(fā)生溢出時,可以不存儲到堆棧而存儲到 XMM 寄存器中嗎?答案是可以。這么做會帶來什么好處?

4. 實驗

看看下面這個簡單的 JMH 基準測試,用一種非常特殊的方式構(gòu)建基準(簡單起見,這里假設(shè) Java 具備有預(yù)處理能力):

import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class FPUSpills {
    int s00, s01, s02, s03, s04, s05, s06, s07, s08, s09;
    int s10, s11, s12, s13, s14, s15, s16, s17, s18, s19;
    int s20, s21, s22, s23, s24;
    int d00, d01, d02, d03, d04, d05, d06, d07, d08, d09;
    int d10, d11, d12, d13, d14, d15, d16, d17, d18, d19;
    int d20, d21, d22, d23, d24;
    int sg;
    volatile int vsg;
    int dg;
    @Benchmark
#ifdef ORDERED
    public void ordered() {
#else
    public void unordered() {
#endif
        int v00 = s00; int v01 = s01; int v02 = s02; int v03 = s03; int v04 = s04;
        int v05 = s05; int v06 = s06; int v07 = s07; int v08 = s08; int v09 = s09;
        int v10 = s10; int v11 = s11; int v12 = s12; int v13 = s13; int v14 = s14;
        int v15 = s15; int v16 = s16; int v17 = s17; int v18 = s18; int v19 = s19;
        int v20 = s20; int v21 = s21; int v22 = s22; int v23 = s23; int v24 = s24;
#ifdef ORDERED
        dg = vsg; // 給 optimizer 制造點麻煩
#else
        dg = sg;  // 只做常規(guī)存儲
#endif
        d00 = v00; d01 = v01; d02 = v02; d03 = v03; d04 = v04;
        d05 = v05; d06 = v06; d07 = v07; d08 = v08; d09 = v09;
        d10 = v10; d11 = v11; d12 = v12; d13 = v13; d14 = v14;
        d15 = v15; d16 = v16; d17 = v17; d18 = v18; d19 = v19;
        d20 = v20; d21 = v21; d22 = v22; d23 = v23; d24 = v24;
    }
}

上面的例子中一次會讀寫多對字段。實際上,優(yōu)化器本身并不會與具體程序綁定。事實上,這就是在 unordered 測試中觀察到的結(jié)果:

Benchmark                                  Mode  Cnt   Score    Error  Units
FPUSpills.unordered                        avgt   15   6.961 ±  0.002  ns/op
FPUSpills.unordered:CPI                    avgt    3   0.458 ±  0.024   #/op
FPUSpills.unordered:L1-dcache-loads        avgt    3  28.057 ±  0.730   #/op
FPUSpills.unordered:L1-dcache-stores       avgt    3  26.082 ±  1.235   #/op
FPUSpills.unordered:cycles                 avgt    3  26.165 ±  1.575   #/op
FPUSpills.unordered:instructions           avgt    3  57.099 ±  0.971   #/op

上面展示了26對 load-store,實際測試中大致有25對,但是這里沒有25個通用寄存器!從 perfasm 結(jié)果中可以看到,優(yōu)化器會把臨近的 load-store 對合并,減小寄存器壓力:

0.38%    0.28%  ↗  movzbl 0x94(%rcx),%r9d
                │  ...
0.25%    0.20%  │  mov    0xc(%r11),%r10d    ; 讀取字段 s00
0.04%    0.02%  │  mov    %r10d,0x70(%r8)    ; 存儲字段 d00
                │  ...
                │  ... (transfer repeats for multiple vars) ...
                │  ...
                ╰  je     BACK

ordered 測試會給優(yōu)化器制造一點混亂,在存儲前全部加載。上面的結(jié)果也印證了這一點:先全部加載,再全部存儲。加載全部完成時寄存器的壓力最大,這時還沒有開始存儲。即便如此,從結(jié)果來看與 unordered 差異不大:

Benchmark                                  Mode  Cnt   Score    Error  Units
FPUSpills.unordered                        avgt   15   6.961 ±  0.002  ns/op
FPUSpills.unordered:CPI                    avgt    3   0.458 ±  0.024   #/op
FPUSpills.unordered:L1-dcache-loads        avgt    3  28.057 ±  0.730   #/op
FPUSpills.unordered:L1-dcache-stores       avgt    3  26.082 ±  1.235   #/op
FPUSpills.unordered:cycles                 avgt    3  26.165 ±  1.575   #/op
FPUSpills.unordered:instructions           avgt    3  57.099 ±  0.971   #/op
FPUSpills.ordered                          avgt   15   7.961 ±  0.008  ns/op
FPUSpills.ordered:CPI                      avgt    3   0.329 ±  0.026   #/op
FPUSpills.ordered:L1-dcache-loads          avgt    3  29.070 ±  1.361   #/op
FPUSpills.ordered:L1-dcache-stores         avgt    3  26.131 ±  2.243   #/op
FPUSpills.ordered:cycles                   avgt    3  30.065 ±  0.821   #/op
FPUSpills.ordered:instructions             avgt    3  91.449 ±  4.839   #/op

這是因為已經(jīng)設(shè)法把操作數(shù)溢出到 XMM 寄存器中,而不是在堆棧上存儲:

3.08%    3.79%  ↗  vmovq  %xmm0,%r11
                │  ...
0.25%    0.20%  │  mov    0xc(%r11),%r10d    ; 讀取字段 s00
0.02%           │  vmovd  %r10d,%xmm4        ; < --- FPU 溢出
0.25%    0.20%  │  mov    0x10(%r11),%r10d   ; 讀取字段 s01
0.02%           │  vmovd  %r10d,%xmm5        ; < --- FPU 溢出
                │  ...
                │  ... (讀取更多字段和 XMM 溢出) ...
                │  ...
0.12%    0.02%  │  mov    0x60(%r10),%r13d   ; 讀取字段 s21
                │  ...
                │  ... (讀取到寄存器) ...
                │  ...
                │  ------- 讀取完成, 開始寫操作 ------
0.18%    0.16%  │  mov    %r13d,0xc4(%rdi)   ; 存儲字段 d21
                │  ...
                │  ... (讀寄存器并存儲字段)
                │  ...
2.77%    3.10%  │  vmovd  %xmm5,%r11d        : < --- FPU 取消溢出
0.02%           │  mov    %r11d,0x78(%rdi)   ; 存儲字段 d01
2.13%    2.34%  │  vmovd  %xmm4,%r11d        ; < --- FPU 取消溢出
0.02%           │  mov    %r11d,0x70(%rdi)   ; 存儲字段 d00
                │  ...
                │  ... (取消溢出并存儲字段)
                │  ...
                ╰  je     BACK

請注意:這里的確對某些操作數(shù)使用了通用寄存器(GPR),但是當所有寄存器被用完時會發(fā)生溢出。這里對時機的描述并不確切??雌饋硐劝l(fā)生了溢出,然后使用 GPR。然而這是一個假象,因為寄存器分配器是在全局進行分配。

(2) 一些寄存器分配器實際執(zhí)行的是線性分配,提高了 regalloc 的速度與生成代碼的效率。

XMM 溢出延遲似乎是最小的:盡管溢出需要更多指令,但它們的執(zhí)行效率很高能夠有效彌補流水線的缺陷。通過34條額外指令,大約17條溢出指令對,實際只要求4個額外周期。請注意,按照 4/34 = ~0.11 時鐘/指令 計算 CPI 是不對的,計算結(jié)果會超出當前 CPU 處理能力。但是實際帶來的改進是真實的,因為使用了以前沒有用到的執(zhí)行塊。

沒有參照談效率是毫無意義的。這里用 -XX:-UseFPUForSpilling 讓 Hotspot 禁用 FPU 溢出,這樣可以了解 XMM 溢出帶來的好處:

Benchmark                                  Mode  Cnt   Score    Error  Units
# Default
FPUSpills.ordered                          avgt   15   7.961 ±  0.008  ns/op
FPUSpills.ordered:CPI                      avgt    3   0.329 ±  0.026   #/op
FPUSpills.ordered:L1-dcache-loads          avgt    3  29.070 ±  1.361   #/op
FPUSpills.ordered:L1-dcache-stores         avgt    3  26.131 ±  2.243   #/op
FPUSpills.ordered:cycles                   avgt    3  30.065 ±  0.821   #/op
FPUSpills.ordered:instructions             avgt    3  91.449 ±  4.839   #/op
# -XX:-UseFPUForSpilling
FPUSpills.ordered                          avgt   15  10.976 ±  0.003  ns/op
FPUSpills.ordered:CPI                      avgt    3   0.455 ±  0.053   #/op
FPUSpills.ordered:L1-dcache-loads          avgt    3  47.327 ±  5.113   #/op
FPUSpills.ordered:L1-dcache-stores         avgt    3  41.078 ±  1.887   #/op
FPUSpills.ordered:cycles                   avgt    3  41.553 ±  2.641   #/op
FPUSpills.ordered:instructions             avgt    3  91.264 ±  7.312   #/op

上面的結(jié)果可以看到 load/store 計數(shù)增加,為什么?這些是堆棧溢出。雖然堆棧本身速度很快,但仍然在內(nèi)存中運行,訪問 L1 緩存中的堆??臻g?;旧洗蠹s需要額外17個存儲對,但現(xiàn)在只需要約11個時鐘周期。這里 L1 緩存的吞吐量是主要限制。

最后,可以觀察 -XX:-UseFPUForSpilling 的 perfasm 輸出:

2.45%    1.21%  ↗  mov    0x70(%rsp),%r11
                │  ...
0.50%    0.31%  │  mov    0xc(%r11),%r10d    ; 讀取字段 s00
0.02%           │  mov    %r10d,0x10(%rsp)   ; < --- 堆棧溢出!
2.04%    1.29%  │  mov    0x10(%r11),%r10d   ; 讀取字段 s01
                │  mov    %r10d,0x14(%rsp)   ; < --- 堆棧溢出!
                │  ...
                │  ... (讀取其它字段和堆棧溢出) ...
                │  ...
0.12%    0.19%  │  mov    0x64(%r10),%ebp    ; 讀取字段 s22
                │  ...
                │  ... (more reads into registers) ...
                │  ...
                │  ------- 讀取完成, 開始寫操作 ------
3.47%    4.45%  │  mov    %ebp,0xc8(%rdi)    ; 存儲字段 d22
                │  ...
                │  ... (讀取更多寄存器和存儲字段)
                │  ...
1.81%    2.68%  │  mov    0x14(%rsp),%r10d   ; < --- 取消堆棧溢出
0.29%    0.13%  │  mov    %r10d,0x78(%rdi)   ; 存儲字段 d01
2.10%    2.12%  │  mov    0x10(%rsp),%r10d   ; < --- 取消堆棧溢出
                │  mov    %r10d,0x70(%rdi)   ; 存儲字段 d00
                │  ...
                │  ... (取消其它溢出和存儲字段)
                │  ...
                ╰  je     BACK

的確,在堆棧溢出發(fā)生的地方也可以看到 XMM 溢出。

5. 觀察

FPU 溢出是緩解寄存器壓力的一種好辦法。雖然不增加通用寄存器寄存器數(shù)量,但確實在溢出時提供了更快的臨時存儲。在僅需要幾個額外的溢出存儲時,可以避免轉(zhuǎn)存到 L1 緩存支持的堆棧。

這為什么有時會出現(xiàn)奇怪的性能差異:如果在一些關(guān)鍵路徑上沒有用到 FPU 溢出,很可能會看到性能下降。例如,引入一個 slow-path GC 屏障,假定會清除 FPU 寄存器,可能會讓編譯器回退到堆棧溢出,并不去嘗試其它優(yōu)化。

對支持 SSE 的 x86 平臺、ARMv7 和 AArch64,Hotspot 默認啟用 -XX:+UseFPUForSpilling。因此,無論是否知道這個技巧,大多數(shù)程序都能從中受益。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • ARM
    ARM
    +關(guān)注

    關(guān)注

    134

    文章

    8967

    瀏覽量

    365059
  • 寄存器
    +關(guān)注

    關(guān)注

    31

    文章

    5253

    瀏覽量

    119212
  • 存儲器
    +關(guān)注

    關(guān)注

    38

    文章

    7366

    瀏覽量

    163099
  • JVM
    JVM
    +關(guān)注

    關(guān)注

    0

    文章

    155

    瀏覽量

    12168
  • FPU
    FPU
    +關(guān)注

    關(guān)注

    0

    文章

    41

    瀏覽量

    21211
收藏 人收藏

    評論

    相關(guān)推薦

    基于ARM嵌入式平臺X86譯碼SOC架構(gòu)設(shè)計

    向解碼模塊發(fā)送信號,通過設(shè)置Communicate模塊中的寄存器控制指令譯碼的工作:設(shè)置X86指令的起始地址;設(shè)置X86指令的終止地址;設(shè)置ARM指令的初始存放地址;設(shè)置ARM指令復(fù)
    發(fā)表于 11-08 14:37

    X86硬件設(shè)計系列知識分享

    論壇有很多嵌入式的技術(shù)資料,卻幾乎沒有X86硬件(PC,Server 等等)設(shè)計技術(shù)資料。本人從事X86硬件及系統(tǒng)設(shè)計多年,總結(jié)了系列X86平臺設(shè)計知識,逐步分享出來,期望能夠?qū)氖孪?/div>
    發(fā)表于 10-17 12:18

    解讀x86、ARM和MIPS三種主流芯片架構(gòu)

    代碼記憶體里執(zhí)行,新設(shè)計的處理,只需增加較少的電晶體就可以執(zhí)行同樣的指令集,也可以很快地編寫新的指令集程式;二是擁有龐大的指令集,x86擁有包括雙運算元格式、寄存器
    發(fā)表于 05-25 16:09

    x86平臺架構(gòu)如何為用戶帶來豐富的交互式駕駛體驗?

    本文主要講述了x86平臺架構(gòu)如何為用戶帶來豐富的交互式駕駛體驗,而這是非PC兼容型平臺難以實現(xiàn)的。
    發(fā)表于 05-14 06:45

    嵌入式X86和ARM各自都有哪些優(yōu)缺點呢

    代碼記憶體里執(zhí)行;二是擁有龐大的指令集,擁有包括雙運算元格式、寄存器寄存器寄存器到記憶體以及記憶體到寄存器的多種指令類型。
    發(fā)表于 12-14 09:21

    TarsARM平臺上的移植是如何去實現(xiàn)的

    計時來實現(xiàn),具體實現(xiàn)如下。原x86嵌匯編實現(xiàn):支持ARM64平臺后的實現(xiàn):3 協(xié)程實現(xiàn)協(xié)程是一種用戶態(tài)的輕量級線程,其調(diào)度完全由用戶控制。因此,協(xié)程調(diào)度切換時需要用戶自己將寄存器和棧
    發(fā)表于 03-30 11:30

    arm64和x86服務(wù)上運行的耗時來發(fā)現(xiàn)Dockerarm64架構(gòu)下的性能問題

    發(fā)現(xiàn)Dockerarm64架構(gòu)下的性能問題。本文描述的性能測試是分別在一arm64和一x86服務(wù)上進行的,兩個服務(wù)
    發(fā)表于 07-12 15:48

    英特爾雙核處理 進軍x86平臺市場

        英特爾(Intel)于15日宣布Core Duo處理(Yonah雙核心處理)進軍x86嵌入式平臺市場,
    發(fā)表于 03-13 13:00 ?565次閱讀

    LinuxBIOS嵌入式x86系統(tǒng)中的配置

    LinuxBIOS嵌入式x86系統(tǒng)中的配置   引言   嵌入式計算機系統(tǒng)隨著其應(yīng)用方式不同,具有不同的體系結(jié)構(gòu)形式?;?b class='flag-5'>x86平臺的嵌入式計算機
    發(fā)表于 12-11 10:00 ?879次閱讀

    X86平臺嵌入式軟件應(yīng)用

    X86平臺嵌入式軟件應(yīng)用
    發(fā)表于 01-14 02:36 ?0次下載

    如何在C代碼中插入寄存器

    寄存。這起到了隔離關(guān)鍵路徑的作用。 但是,如果使用的RTL代碼是HLS轉(zhuǎn)換生成的,例如使用Vitis HLS綜合的,其可讀性較差,想要在其生成的HDL
    的頭像 發(fā)表于 02-02 17:07 ?2964次閱讀
    如何在C<b class='flag-5'>代碼</b>中插入<b class='flag-5'>寄存器</b>?

    微機原理筆記——x86寄存器

    8086 CPU中寄存器總共為14個,且均為16位。即 AX,BX,CX,DX,SP,BP,SI,DI,IP,F(xiàn)LAG,CS,DS,SS,ES 共 14 個。而這 14 個寄存器按照一定方式又分
    發(fā)表于 12-08 18:21 ?7次下載
    微機原理筆記——<b class='flag-5'>x86</b><b class='flag-5'>寄存器</b>

    基于X86平臺的ARM指令集模擬設(shè)計

    電子發(fā)燒友網(wǎng)站提供《基于X86平臺的ARM指令集模擬設(shè)計.pdf》資料免費下載
    發(fā)表于 10-11 15:03 ?0次下載
    基于<b class='flag-5'>X86</b><b class='flag-5'>平臺</b>的ARM指令集模擬<b class='flag-5'>器</b>設(shè)計

    基于X86平臺的ARM指令集模擬的設(shè)計方案

    電子發(fā)燒友網(wǎng)站提供《基于X86平臺的ARM指令集模擬的設(shè)計方案.pdf》資料免費下載
    發(fā)表于 11-06 11:54 ?0次下載
    基于<b class='flag-5'>X86</b><b class='flag-5'>平臺</b>的ARM指令集模擬<b class='flag-5'>器</b>的設(shè)計方案

    觸翔X86工控主板交互機器人中的應(yīng)用

    X86工控主板交互機器人中的應(yīng)用主要體現(xiàn)在其高可靠性、抗干擾性和穩(wěn)定性等方面,為交互機器人的正常運行和精確控制提供了有力支持。以下是關(guān)于X86
    的頭像 發(fā)表于 05-28 15:48 ?316次閱讀