CPU 組建選項#

描述#

以下選項主要用於變更針對特定 CPU 功能的最佳化預設行為

  • cpu-baseline:所需的最低 CPU 功能集。

    預設值為 min,其提供可在處理器系列中各種平台上安全執行的最低 CPU 功能。

    請注意

    在執行階段期間,如果目標 CPU 不支援任何指定的功能,NumPy 模組將無法載入(引發 Python 執行階段錯誤)。

  • cpu-dispatch:調度的額外 CPU 功能集。

    預設值為 max -xop -fma4,其啟用所有 CPU 功能,AMD 舊版功能除外(在 X86 的情況下)。

    請注意

    在執行階段期間,NumPy 模組將略過目標 CPU 中不可用的任何指定功能。

這些選項可在組建時透過將設定引數傳遞給 meson-python,經由組建前端(例如,pipbuild)存取。它們接受一組CPU 功能或功能群組,這些群組收集多個功能或特殊選項,這些選項執行一系列程序。

自訂 CPU/組建選項

pip install . -Csetup-args=-Dcpu-baseline="avx2 fma3" -Csetup-args=-Dcpu-dispatch="max"

快速開始#

一般而言,預設設定傾向於不強加某些 CPU 功能,這些功能可能在某些較舊的處理器上不可用。提高基準功能的上限通常會提高效能,也可能會縮減二進位大小。

以下是可能需要變更預設設定的最常見情境

我正在為我的本地使用建置 NumPy#

而且我不打算將組建匯出給其他使用者,或以與主機不同的 CPU 為目標。

為基準設定 native,或在您的平台不支援選項 native 的情況下,手動指定 CPU 功能

python -m build --wheel -Csetup-args=-Dcpu-baseline="native"

在這種情況下,使用額外 CPU 功能建置 NumPy 並非必要,因為所有支援的功能都已在基準功能中定義

python -m build --wheel -Csetup-args=-Dcpu-baseline="native" \
-Csetup-args=-Dcpu-dispatch="none"

請注意

如果主機平台不支援 native,則會引發嚴重錯誤。

我不想支援 x86 架構的舊處理器#

由於現在大多數 CPU 至少支援 AVXF16C 功能,您可以使用

python -m build --wheel -Csetup-args=-Dcpu-baseline="avx f16c"

請注意

cpu-baseline 強制組合所有隱含功能,因此無需新增 SSE 功能。

我遇到與上述相同的情況,但使用 ppc64 架構#

然後將基準功能的上限提高到 Power8

python -m build --wheel -Csetup-args=-Dcpu-baseline="vsx2"

使用 AVX512 功能時遇到問題?#

您可能對包含 AVX512 或任何其他 CPU 功能有所保留,並且想要從調度的功能中排除它們

python -m build --wheel -Csetup-args=-Dcpu-dispatch="max -avx512f -avx512cd \
-avx512_knl -avx512_knm -avx512_skx -avx512_clx -avx512_cnl -avx512_icl"

支援的功能#

功能的名稱可以表示一個功能或一組功能,如下表所示,支援的功能取決於最低的關注度

請注意

以下功能可能並非所有編譯器都支援,而且當涉及到 AVX512AVX2FMA3 等功能時,某些編譯器可能會產生不同的隱含功能集。請參閱平台差異以取得更多詳細資訊。

在 x86 上#

名稱

隱含

收集

SSE

SSE2

SSE2

SSE

SSE3

SSE SSE2

SSSE3

SSE SSE2 SSE3

SSE41

SSE SSE2 SSE3 SSSE3

POPCNT

SSE SSE2 SSE3 SSSE3 SSE41

SSE42

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT

AVX

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42

XOP

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX

FMA4

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX

F16C

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX

FMA3

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C

AVX2

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C

AVX512F

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2

AVX512CD

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F

AVX512_KNL

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD

AVX512ER AVX512PF

AVX512_KNM

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_KNL

AVX5124FMAPS AVX5124VNNIW AVX512VPOPCNTDQ

AVX512_SKX

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD

AVX512VL AVX512BW AVX512DQ

AVX512_CLX

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX

AVX512VNNI

AVX512_CNL

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX

AVX512IFMA AVX512VBMI

AVX512_ICL

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL

AVX512VBMI2 AVX512BITALG AVX512VPOPCNTDQ

AVX512_SPR

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL

AVX512FP16

在 IBM/POWER 大端位元組序上#

名稱

隱含

VSX

VSX2

VSX

VSX3

VSX VSX2

VSX4

VSX VSX2 VSX3

在 IBM/POWER 小端位元組序上#

名稱

隱含

VSX

VSX2

VSX2

VSX

VSX3

VSX VSX2

VSX4

VSX VSX2 VSX3

在 ARMv7/A32 上#

名稱

隱含

NEON

NEON_FP16

NEON

NEON_VFPV4

NEON NEON_FP16

ASIMD

NEON NEON_FP16 NEON_VFPV4

ASIMDHP

NEON NEON_FP16 NEON_VFPV4 ASIMD

ASIMDDP

NEON NEON_FP16 NEON_VFPV4 ASIMD

ASIMDFHM

NEON NEON_FP16 NEON_VFPV4 ASIMD ASIMDHP

在 ARMv8/A64 上#

名稱

隱含

NEON

NEON_FP16 NEON_VFPV4 ASIMD

NEON_FP16

NEON NEON_VFPV4 ASIMD

NEON_VFPV4

NEON NEON_FP16 ASIMD

ASIMD

NEON NEON_FP16 NEON_VFPV4

ASIMDHP

NEON NEON_FP16 NEON_VFPV4 ASIMD

ASIMDDP

NEON NEON_FP16 NEON_VFPV4 ASIMD

ASIMDFHM

NEON NEON_FP16 NEON_VFPV4 ASIMD ASIMDHP

在 IBM/ZSYSTEM(S390X) 上#

名稱

隱含

VX

VXE

VX

VXE2

VX VXE

特殊選項#

  • NONE:不啟用任何功能。

  • NATIVE:啟用主機 CPU 支援的所有 CPU 功能,此操作基於編譯器旗標 (-march=native-xHost/QxHost)

  • MIN:啟用可在各種平台上安全執行的最低 CPU 功能

    適用於架構

    隱含

    x86 (32 位元模式)

    SSE SSE2

    x86_64

    SSE SSE2 SSE3

    IBM/POWER (大端位元組序模式)

    NONE

    IBM/POWER (小端位元組序模式)

    VSX VSX2

    ARMHF

    NONE

    ARM64 A.K. AARCH64

    NEON NEON_FP16 NEON_VFPV4 ASIMD

    IBM/ZSYSTEM(S390X)

    NONE

  • MAX:啟用編譯器和平台支援的所有 CPU 功能。

  • Operators-/+:移除或新增功能,適用於選項 MAXMINNATIVE

行為#

  • CPU 功能和其他選項不區分大小寫,例如

    python -m build --wheel -Csetup-args=-Dcpu-dispatch="SSE41 avx2 FMA3"
    
  • 請求的最佳化順序無關緊要

    python -m build --wheel -Csetup-args=-Dcpu-dispatch="SSE41 AVX2 FMA3"
    # equivalent to
    python -m build --wheel -Csetup-args=-Dcpu-dispatch="FMA3 AVX2 SSE41"
    
  • 逗號或空格或 '+' 都可用作分隔符號,例如

    python -m build --wheel -Csetup-args=-Dcpu-dispatch="avx2 avx512f"
    # or
    python -m build --wheel -Csetup-args=-Dcpu-dispatch=avx2,avx512f
    # or
    python -m build --wheel -Csetup-args=-Dcpu-dispatch="avx2+avx512f"
    

    全部都適用,但如果使用任何空格,則引數應以引號括住或以反斜線跳脫。

  • cpu-baseline 組合所有隱含的 CPU 功能,例如

    python -m build --wheel -Csetup-args=-Dcpu-baseline=sse42
    # equivalent to
    python -m build --wheel -Csetup-args=-Dcpu-baseline="sse sse2 sse3 ssse3 sse41 popcnt sse42"
    
  • 如果透過環境變數 CFLAGS 啟用編譯器原生旗標 -march=native-xHost/QxHost,則 cpu-baseline 將被視為「native」

    export CFLAGS="-march=native"
    pip install .
    # is equivalent to
    pip install . -Csetup-args=-Dcpu-baseline=native
    
  • cpu-baseline 會跳脫任何目標平台或編譯器不支援的指定功能,而不是引發嚴重錯誤。

    請注意

    由於 cpu-baseline 組合所有隱含功能,因此將啟用隱含功能的最大支援,而不是全部跳脫。例如

    # Requesting `AVX2,FMA3` but the compiler only support **SSE** features
    python -m build --wheel -Csetup-args=-Dcpu-baseline="avx2 fma3"
    # is equivalent to
    python -m build --wheel -Csetup-args=-Dcpu-baseline="sse sse2 sse3 ssse3 sse41 popcnt sse42"
    
  • cpu-dispatch 不會組合任何隱含的 CPU 功能,因此您必須新增它們,除非您想要停用其中一個或全部

    # Only dispatches AVX2 and FMA3
    python -m build --wheel -Csetup-args=-Dcpu-dispatch=avx2,fma3
    # Dispatches AVX and SSE features
    python -m build --wheel -Csetup-args=-Dcpu-dispatch=ssse3,sse41,sse42,avx,avx2,fma3
    
  • cpu-dispatch 會跳脫任何指定的基準功能,並且也會跳脫目標平台或編譯器不支援的任何功能,而不會引發嚴重錯誤。

最終,您應始終檢查組建記錄中的最終報告,以驗證已啟用的功能。請參閱組建報告以取得更多詳細資訊。

平台差異#

當涉及到某些編譯器或架構時,某些特殊條件會迫使我們將某些功能連結在一起,導致無法單獨組建它們。

這些條件可分為兩部分,如下所示

架構相容性

需要對齊同一架構的後續世代保證支援的某些 CPU 功能,某些情況

  • 在 ppc64le 上,VSX(ISA 2.06)VSX2(ISA 2.07) 都互相隱含,因為支援小端位元組序模式的第一代是 Power-8`(ISA 2.07)`

  • 在 AArch64 上,NEON NEON_FP16 NEON_VFPV4 ASIMD 互相隱含,因為它們是硬體基準的一部分。

例如

# On ARMv8/A64, specify NEON is going to enable Advanced SIMD
# and all predecessor extensions
python -m build --wheel -Csetup-args=-Dcpu-baseline=neon
# which is equivalent to
python -m build --wheel -Csetup-args=-Dcpu-baseline="neon neon_fp16 neon_vfpv4 asimd"

請注意

請深入查看支援的功能,以判斷互相隱含的功能。

編譯相容性

某些編譯器不為所有 CPU 功能提供獨立支援。例如,Intel 的編譯器不為 AVX2FMA3 提供單獨的旗標,這是合理的,因為所有隨附 AVX2 的 Intel CPU 也支援 FMA3,但這種方法與來自 AMDVIA 的其他 x86 CPU 不相容。

例如

# Specify AVX2 will force enables FMA3 on Intel compilers
python -m build --wheel -Csetup-args=-Dcpu-baseline=avx2
# which is equivalent to
python -m build --wheel -Csetup-args=-Dcpu-baseline="avx2 fma3"

下表僅顯示某些編譯器從支援的功能表格中顯示的一般環境設定所強加的差異

請注意

帶刪除線的功能名稱表示不支援的 CPU 功能。

在 x86::Intel 編譯器上#

名稱

隱含

收集

FMA3

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C AVX2

AVX2

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3

AVX512F

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512CD

XOP

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX

FMA4

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX

AVX512_SPR

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL

AVX512FP16

在 x86::Microsoft Visual C/C++ 上#

名稱

隱含

收集

FMA3

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C AVX2

AVX2

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3

AVX512F

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512CD AVX512_SKX

AVX512CD

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512_SKX

AVX512_KNL

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD

AVX512ER AVX512PF

AVX512_KNM

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_KNL

AVX5124FMAPS AVX5124VNNIW AVX512VPOPCNTDQ

AVX512_SPR

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL

AVX512FP16

組建報告#

在大多數情況下,CPU 組建選項不會產生任何導致組建掛起的嚴重錯誤。組建記錄中可能出現的大多數錯誤都作為嚴重警告,因為編譯器缺少某些預期的 CPU 功能。

因此,我們強烈建議檢查最終報告記錄,以了解啟用了哪些 CPU 功能以及哪些未啟用。

您可以在組建記錄的末尾找到 CPU 最佳化的最終報告,以下是在 x86_64/gcc 上的外觀

########### EXT COMPILER OPTIMIZATION ###########
Platform      :
  Architecture: x64
  Compiler    : gcc

CPU baseline  :
  Requested   : 'min'
  Enabled     : SSE SSE2 SSE3
  Flags       : -msse -msse2 -msse3
  Extra checks: none

CPU dispatch  :
  Requested   : 'max -xop -fma4'
  Enabled     : SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_KNL AVX512_KNM AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL
  Generated   :
              :
  SSE41       : SSE SSE2 SSE3 SSSE3
  Flags       : -msse -msse2 -msse3 -mssse3 -msse4.1
  Extra checks: none
  Detect      : SSE SSE2 SSE3 SSSE3 SSE41
              : build/src.linux-x86_64-3.9/numpy/_core/src/umath/loops_arithmetic.dispatch.c
              : numpy/_core/src/umath/_umath_tests.dispatch.c
              :
  SSE42       : SSE SSE2 SSE3 SSSE3 SSE41 POPCNT
  Flags       : -msse -msse2 -msse3 -mssse3 -msse4.1 -mpopcnt -msse4.2
  Extra checks: none
  Detect      : SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42
              : build/src.linux-x86_64-3.9/numpy/_core/src/_simd/_simd.dispatch.c
              :
  AVX2        : SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C
  Flags       : -msse -msse2 -msse3 -mssse3 -msse4.1 -mpopcnt -msse4.2 -mavx -mf16c -mavx2
  Extra checks: none
  Detect      : AVX F16C AVX2
              : build/src.linux-x86_64-3.9/numpy/_core/src/umath/loops_arithm_fp.dispatch.c
              : build/src.linux-x86_64-3.9/numpy/_core/src/umath/loops_arithmetic.dispatch.c
              : numpy/_core/src/umath/_umath_tests.dispatch.c
              :
  (FMA3 AVX2) : SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C
  Flags       : -msse -msse2 -msse3 -mssse3 -msse4.1 -mpopcnt -msse4.2 -mavx -mf16c -mfma -mavx2
  Extra checks: none
  Detect      : AVX F16C FMA3 AVX2
              : build/src.linux-x86_64-3.9/numpy/_core/src/_simd/_simd.dispatch.c
              : build/src.linux-x86_64-3.9/numpy/_core/src/umath/loops_exponent_log.dispatch.c
              : build/src.linux-x86_64-3.9/numpy/_core/src/umath/loops_trigonometric.dispatch.c
              :
  AVX512F     : SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2
  Flags       : -msse -msse2 -msse3 -mssse3 -msse4.1 -mpopcnt -msse4.2 -mavx -mf16c -mfma -mavx2 -mavx512f
  Extra checks: AVX512F_REDUCE
  Detect      : AVX512F
              : build/src.linux-x86_64-3.9/numpy/_core/src/_simd/_simd.dispatch.c
              : build/src.linux-x86_64-3.9/numpy/_core/src/umath/loops_arithm_fp.dispatch.c
              : build/src.linux-x86_64-3.9/numpy/_core/src/umath/loops_arithmetic.dispatch.c
              : build/src.linux-x86_64-3.9/numpy/_core/src/umath/loops_exponent_log.dispatch.c
              : build/src.linux-x86_64-3.9/numpy/_core/src/umath/loops_trigonometric.dispatch.c
              :
  AVX512_SKX  : SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD
  Flags       : -msse -msse2 -msse3 -mssse3 -msse4.1 -mpopcnt -msse4.2 -mavx -mf16c -mfma -mavx2 -mavx512f -mavx512cd -mavx512vl -mavx512bw -mavx512dq
  Extra checks: AVX512BW_MASK AVX512DQ_MASK
  Detect      : AVX512_SKX
              : build/src.linux-x86_64-3.9/numpy/_core/src/_simd/_simd.dispatch.c
              : build/src.linux-x86_64-3.9/numpy/_core/src/umath/loops_arithmetic.dispatch.c
              : build/src.linux-x86_64-3.9/numpy/_core/src/umath/loops_exponent_log.dispatch.c
CCompilerOpt.cache_flush[804] : write cache to path -> /home/seiko/work/repos/numpy/build/temp.linux-x86_64-3.9/ccompiler_opt_cache_ext.py

########### CLIB COMPILER OPTIMIZATION ###########
Platform      :
  Architecture: x64
  Compiler    : gcc

CPU baseline  :
  Requested   : 'min'
  Enabled     : SSE SSE2 SSE3
  Flags       : -msse -msse2 -msse3
  Extra checks: none

CPU dispatch  :
  Requested   : 'max -xop -fma4'
  Enabled     : SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_KNL AVX512_KNM AVX512_SKX AVX512_CLX AVX512_CNL AVX512_ICL
  Generated   : none

每個 build_extbuild_clib 都有一個單獨的報告,其中包括幾個章節,每個章節都有幾個值,代表以下內容

平台:

  • 架構:目標 CPU 的架構名稱。它應為 x86x64ppc64ppc64learmhfaarch64s390xunknown 之一。

  • 編譯器:編譯器名稱。它應為 gcc、clang、msvc、icc、iccw 或 unix-like 之一。

CPU 基準:

  • 請求cpu-baseline 的特定功能和選項,保持原樣。

  • 已啟用:最終的已啟用 CPU 功能集。

  • 旗標:用於所有 NumPy C/C++ 原始碼編譯期間的編譯器旗標,但用於產生調度功能的二進位物件的暫時原始碼除外。

  • 額外檢查:啟動與已啟用功能相關的某些功能或內建函數的內部檢查清單,在開發 SIMD 核心時對於偵錯很有用。

CPU 調度:

  • 請求cpu-dispatch 的特定功能和選項,保持原樣。

  • 已啟用:最終的已啟用 CPU 功能集。

  • 已產生:在此屬性的下一行開頭,已產生最佳化的功能以幾個章節的形式顯示,這些章節具有類似的屬性,如下所述

    • 一個或多個調度功能:隱含的 CPU 功能。

    • 旗標:用於這些功能的編譯器旗標。

    • 額外檢查:與基準類似,但適用於這些調度功能。

    • 偵測:執行產生的最佳化時需要在執行階段偵測的 CPU 功能集。

    • 在上述屬性之後且以 ':' 在單獨一行結尾的行,表示定義產生的最佳化的 c/c++ 原始碼路徑。

執行階段調度#

匯入 NumPy 會觸發掃描可調度功能集中可用的 CPU 功能。這可以透過將環境變數 NPY_DISABLE_CPU_FEATURES 設定為要停用的功能(以逗號、Tab 或空格分隔的清單)來進一步限制。如果剖析失敗或功能未啟用,這將引發錯誤。例如,在 x86_64 上,這將停用 AVX2FMA3

NPY_DISABLE_CPU_FEATURES="AVX2,FMA3"

如果功能不可用,則會發出警告。

追蹤調度的函數#

透過 Python 函數 numpy.lib.introspect.opt_func_info 可以發現哪些 CPU 目標已針對不同的最佳化函數啟用。此函數提供使用兩個選用引數套用篩選器的彈性:一個用於精簡函數名稱,另一個用於指定簽章中的資料類型。

例如

 >> func_info = numpy.lib.introspect.opt_func_info(func_name='add|abs', signature='float64|complex64')
 >> print(json.dumps(func_info, indent=2))
 {
   "absolute": {
     "dd": {
       "current": "SSE41",
       "available": "SSE41 baseline(SSE SSE2 SSE3)"
     },
     "Ff": {
       "current": "FMA3__AVX2",
       "available": "AVX512F FMA3__AVX2 baseline(SSE SSE2 SSE3)"
     },
     "Dd": {
       "current": "FMA3__AVX2",
       "available": "AVX512F FMA3__AVX2 baseline(SSE SSE2 SSE3)"
     }
   },
   "add": {
     "ddd": {
       "current": "FMA3__AVX2",
       "available": "FMA3__AVX2 baseline(SSE SSE2 SSE3)"
     },
     "FFF": {
       "current": "FMA3__AVX2",
       "available": "FMA3__AVX2 baseline(SSE SSE2 SSE3)"
     }
  }
}