LoginSignup
1
0

「Linux on Power Porting Guide - Vector Intrinsics by OpenPOWER Foundation」の日本語翻訳版を作成しました

Last updated at Posted at 2024-07-04

「Linux on Power Porting Guide - Vector Intrinsics by OpenPOWER Foundation」を日本語に翻訳してさらにわかりやすくしました。


Linux on Power Porting Guide - Vector Intrinsics

2018年4月11日

OpenPOWER Foundation


序文

このプロジェクトの目的は、Linuxアプリケーションで一般的に使用されるIntel MMX、SSE、およびAVXのIntrinsic関数の機能的な同等物を提供し、それら(または同等物)をPowerPC64LEプラットフォームで利用できるようにすることです。

このドキュメントは、システムソフトウェアワークグループによって所有され、OpenPOWER Foundation Work Group(WG)プロセスドキュメントに記載された要件に従って処理される、非標準トラックのワークグループノートの成果物です。これは、マスターテンプレートガイドバージョン0.9.5を使用して作成されました。コメント、質問などは、このドキュメントのパブリックメーリングリストに送信できます。


目次

  1. Intel Intrinsicポーティングガイド for Power64LE
    1.1. ソースを見よ、Luke
      1.1.1. Intrinsicのインクルード構造
      1.1.2. Intrinsicで使用される型
      1.1.3. APIの実装方法
       1.1.3.1. 一部の簡単な例
       1.1.3.2. 追加の属性について
       1.1.3.3. どうやってこれを見つけたのか?
       1.1.3.4. 他のIntrinsicを使用して実装された例

  2. これをどう使うのか?
    2.1. 推奨される方法
    2.2. 準備する
     2.2.1. GCC ベクトル拡張
     2.2.2. Intel Intrinsic関数
      2.2.2.1. パックド vs スカラー型Intrinsic
      2.2.2.2. vec_not か not
      2.2.2.3. レーンの交差
     2.2.3. PowerISA ベクトル機能
      2.2.3.1. PowerISA
      2.2.3.2. PowerISA ベクトルIntrinsic
      2.2.3.3. ベクトル要素のサイズと型の変更方法
     2.2.4. さらにいくつかのIntrinsicの例

  3. 本質的な違い
    3.1. 浮動小数点例外
    3.2. 浮動小数点丸めモード
    3.3. パフォーマンス
     3.3.2. MMXIntrinsicを使用する

付録A. ドキュメントの参照
付録B. Intel Intrinsic suffixes
付録C. OpenPOWER Foundationの概要


1. Intel Intrinsicポーティングガイド for Power64LE

このプロジェクトの目標は、Linuxアプリケーションで一般的に使用されるIntel MMX、SSE、およびAVXのIntrinsic関数の機能的な同等物を提供し、それら(または同等物)をPowerPC64LEプラットフォームで利用できるようにすることです。これらのX86IntrinsicはIntelおよびMicrosoftのコンパイラで開始されましたが、その後GCCコンパイラに移植されました。GCC実装は、インライン関数を含むヘッダーのセットです。これらのインライン関数は、Intel/Microsoft方言のIntrinsic名から対応するGCC Intelビルトインまたは直接C言語ベクトル拡張構文へのマッピングを提供します。

現在の提案は、既存のX86 GCCIntrinsicヘッダーを使用して、それらを(ソースをコピーして変更し)C言語ベクトル拡張、VMXおよびVSXビルトインを使用してPOWERに移植することです。もう一つの重要な前提は、./gcc/testsuite/gcc.target/i386にある既存のIntel DejaGNUテストケースの多くを使用できるということです。このドキュメントは、この作業に参加する開発者のためのガイドとして意図されています。ただし、このドキュメントは、他のプラットフォームに移植するコードでX86Intrinsicに遭遇する可能性のある開発者にも役立つガイダンスと例を提供します。

注意
(X86IntrinsicヘッダーのGCCプロジェクトへの貢献を開始しました。)現在のプロジェクトの状況は、BMI(bmiintrin.h)、BMI2(bmi2intrin.h)、MMX(mmintrin.h)、およびSSE(xmmintrin.h)IntrinsicヘッダーがGCC開発トランクにコミットされていることです。SSE2(emmintrin.h)の作業が進行中です。

1.1. ソースを見よ、Luke

では、これはコードポーティングの活動ですが、ソースはどこにあるのでしょうか?見る必要のあるソースコードはすべてGCCソースツリーにあります。GCCのソースツリーをgit( https://gcc.gnu.org/wiki/GitMirro )するか、tarファイル(例:ftp://ftp.unicamp.br/pub/linuxpatch/toolchain/at/ubuntu/dists/xenial/at10.0/ )のいずれかをダウンロードすることができます。Intrinsicヘッダーは./gcc/config/i386/サブディレクトリにあります。

Intel LinuxワークステーションまたはGCCがインストールされているラップトップがある場合、これらのヘッダーがすでに含まれています。以下のコマンドを使用して確認できます。

$ find /usr/lib -name '*mmintrin.h'
/usr/lib/gcc/x86_64-redhat-linux/4.4.4/include/wmmintrin.h
/usr/lib/gcc/x86_64-redhat-linux/4.4.4/include/mmintrin.h
/usr/lib/gcc/x86_64-redhat-linux/4.4.4/include/xmmintrin.h
/usr/lib/gcc/x86_64-redhat-linux/4.4.4/include/emmintrin.h
/usr/lib/gcc/x86_64-redhat-linux/4.4.4/include/tmmintrin.h
...
$

しかし、ディストリビューションの年代によっては、これらが最新バージョンのヘッダーではないかもしれません。ヘッダーソースを見て、いくつかのことがわかります:インクルード構造(他のヘッダーが暗黙的に含まれているかどうか)、APIで使用される型、最後にAPIがどのように実装されているかです。

1.1.1. Intrinsicのインクルード構造

GCCのx86Intrinsic関数は、最初は技術ごとにグループ化されていました(MMXおよびSSE)。これは、MMXから始まり、SSE4.1まで続きます。

基本的には、各上位レイヤーのインクルードには、下位レベルのIntrinsicインクルードによって定義されたtypedefおよびヘルパーマクロが必要です。mm_malloc.hは単にposix_memalignとfreeのラッパーを提供します。それから、暗号拡張から始まる少し変わったことが起こります:

wmmintrin.h(AES)はemmintrin.hを含む

AVX、AVX2、およびAVX512については、ロシアの人形のようなものが手に負えなくなってきたと判断したようです。AVXなどは14ファイルに分割されています:

#include <avxintrin.h>
#include <avx2intrin.h>
#include <avx512fintrin.h>
#include <avx512erintrin.h>
#include <avx512pfintrin.h>
#include <avx512cdintrin.h>
#include <avx512vlintrin.h>
#include <avx512bwintrin.h>
#include <avx512dqintrin.h>
#include <avx512vlbwintrin.h>
#include <avx512vldqintrin.h>
#include <avx512ifmaintrin.h>
#include <avx512ifmavlintrin.h>
#include <avx512vbmiintrin.h>
#include <avx512vbmivlintrin.h>

しかし、これらを個別にインクルードすることは望んでいません。そのため、immintrin.hはすべてのIntelベクトルを含むすべてのAVX、AES、SSE、およびMMXフレーバーを含むことになっています。

#ifndef _IMMINTRIN_H_INCLUDED
# error "Never use <avxintrin.h> directly; include <immintrin.h> instead."
#endif

なぜこれが興味深いのでしょうか?インクルード構造は、この作業を進める順序についての強力な手がかりを提供します。例えば、SSE4(smmintrin.h)のIntrinsicを使用する必要がある場合、SSE(emmintrin.h)のtypedefを使用する可能性が高いです。そのため、ボトムアップ(MMX、SSE、SSE2、…)アプローチが最適な計画のように思えます。また、AVX部分を後回しにすることも理にかなっています。なぜなら、ほとんどが既存のSSE操作のより広い形式だからです。

同じインクルード構造を使用して、PowerISAの同等のAPIヘッダーを実装する必要があります。これにより、移植が容易になり(置き換えが容易になり)、アプリケーションが迅速にPOWERで動作するようになります。次に、生成されたアプリケーションのプロファイルと分析を行うことができます。これにより、単純な一対一の変換がボトルネックとなるホットスポットが表示され、追加のチューニングが必要な場合が分かります。これらの場合、ツール(SDK MA/SCA)を改善して、PowerISAおよび我々のマイクロアーキテクチャに最適化された代替シーケンスを特定し、提案する機会を提供すべきです。

1.1.2. Intrinsicで使用される型

IntelIntrinsicの型システムは少し奇妙です。例えば、xmmintrin.hから:

/* The Intel API is flexible enough that we must allow aliasing with other
   vector types, and their scalar components.  */
typedef float __m128 __attribute__ ((__vector_size__ (16), __may_alias__));

/* Internal data types for implementing the intrinsics.  */
typedef float __v4sf __attribute__ ((__vector_size__ (16)));

つまり、APIの関数プロトタイプで使用される型と、実装で使用される内部型があります。__may_alias__属性に注意してください。GCCのドキュメントから:

Accesses through pointers to types with this attribute are not subject to type-
based alias analysis, but are instead assumed to be able to alias any other type of
objects. ... This extension exists to support some vector APIs, in which pointers to
one vector type are permitted to alias pointers to a different vector type.

ここにはいくつかの問題があります:

  • __may_alias__の使用は、参照によって渡された任意のパラメータのエイリアシングをコンパイラに強制するようです。
  • GCCベクトルビルトイン型システム(上記の例)は、元のAltivec __vector型とは少し異なる構文です。内部的には、これらのtypedef形式は同じ128ビットベクトル型を表すかもしれませんが、初期のソース解析とオーバーロードされたベクトルビルトインでは異なって処理されます。
  • インターフェースで使用されるデータ型が、暗黙の操作に対して正しい型でない場合があります。

通常、コンパイラは異なるサイズのパラメータがストレージで重ならないと仮定します。これにより、より多くの最適化が可能になります。しかし、異なるベクトル要素サイズ[char | short | int | long]のパラメータはすべて__m128i(vector long longとして定義)型で渡され、返されます。

このことは、x86のビルトインを使用する場合には問題にならないかもしれませんが、Cベクトル拡張を使用する場合や、我々のケースではPowerPCオーバーロードベクトルビルトインを使用する場合には問題になります(セクション2.2.3.2、「PowerISA Vector Intrinsics」)。後者の場合、型は正しい型でなければならず、コンパイラが正しいコードを生成するためには、(char、short、int、long)型に対してオーバーロードされたビルトイン操作が必要です。また、__may_alias__を過剰に使用すると、コンパイラの最適化が制限される可能性があることも懸念されます。この属性がAPIの正しい動作にどれほど重要であるかは不明です。そのため、後の段階で、PowerPCの実装からこの属性を削除することを試みるべきです。

良いニュースは、PowerISAが128ビットベクトル(VSXの追加により)をサポートしており、必要なすべてのベクトルデータ型(char、short、int、long、float、double)をサポートしていることです。ただし、IntelはPowerISAよりも広範なベクトルサイズをサポートしています。これは64ビットMMXベクトルサポートから始まり、SSE、AVX、AVX2、AVX512の256ビットおよび512ビットベクトルまで拡張されています。

GCCのIntelIntrinsic実装では、これらはすべて適切なサイズのベクトル属性拡張として実装されています(vector_size({8 | 16 | 32、64})。PowerPCターゲットのGCCでは、現在ネイティブな__vector_size__(16)をサポートしています。これらはVMX/VSXレジスタおよび関連する命令で直接サポートできます。

GCCは他の__vector_size__値でコードをコンパイルしますが、結果の型は単純な要素型の配列として扱われます。これにより、コンパイラはパラメータの受け渡しや戻り値にベクトルレジスタを使用できなくなります。例えば、immintrin.hのこのIntrinsic:

typedef double __m256d __attribute__ ((__vector_size__ (32), __may_alias__));

extern __inline __m256d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm256_add_pd (__m256d __A, __m256d __B)
{
  return (__m256d) ((__v4df)__A + (__v4df)__B);
}

テストケース:

__m256d
test_mm256_add_pd (__m256d __A, __m256d __B)
{
  return (_mm256_add_pd (__A, __B));
}

現在のGCCは次のように生成します:

0000000000000970 <test_mm256_add_pd>:
 970: 10 00 20 39 li r9,16
 974: 98 26 80 7d lxvd2x vs12,0,r4
 978: 98 2e 40 7d lxvd2x vs10,0,r5
 97c: 20 00 e0 38 li r7,32
 980: f8 ff e1 fb std r31,-8(r1)
 984: b1 ff 21 f8 stdu r1,-80(r1)
 988: 30 00 00 39 li r8,48
 98c: 98 4e 04 7c lxvd2x vs0,r4,r9
 990: 98 4e 65 7d lxvd2x vs11,r5,r9
 994: 00 53 8c f1 xvadddp vs12,vs12,vs10
 998: 00 00 c1 e8 ld r6,0(r1)
 99c: 78 0b 3f 7c mr r31,r1
 9a0: 00 5b 00 f0 xvadddp vs0,vs0,vs11
 9a4: c1 ff c1 f8 stdu r6,-64(r1)
 9a8: 98 3f 9f 7d stxvd2x vs12,r31,r7
 9ac: 98 47 1f 7c stxvd2x vs0,r31,r8
 9b0: 98 3e 9f 7d lxvd2x vs12,r31,r7
 9b4: 98 46 1f 7c lxvd2x vs0,r31,r8
 9b8: 50 00 3f 38 addi r1,r31,80
 9bc: f8 ff e1 eb ld r31,-8(r1)
 9c0: 98 1f 80 7d stxvd2x vs12,0,r3
 9c4: 98 4f 03 7c stxvd2x vs0,r3,r9
 9c8: 20 00 80 4e blr

コンパイラはパラメータと戻り値をスカラ配列として扱い、参照で渡されます。この場合、操作はベクトル化されていますが、256ビットの結果はストレージを通じて返されます。

これは、単純な4つのダブル加算には不適切です。MMX(セクション1.1.2.1、「Dealing with MMX」)およびAVX(セクション1.1.2.2、「Dealing with AVX and AVX512」)の値をPowerPCレジスタとして渡し、ストレージ参照を避けることができればより良いでしょう。パラメータと戻り値をレジスタとして渡すことができれば、この例は次のように簡略化されます:

0000000000000970 <test_mx256_add_pd>:
 970: xvadddp vs34,vs34,vs36
 974: xvadddp vs35,vs35,vs37
 978: blr

PowerISA VMX/VSXの機能とGCCコンパイラの128ビット/16バイトベクトルおよび関連するベクトルビルトインのサポートは、X86 SSEIntrinsic関数の同等の実装に非常に適しています。しかし、古いMMX(64ビット)および最新のAVX(256/512ビット)拡張の実装には、さらに考慮と工夫が必要です。

1.1.2.1. Dealing with MMX

MMXは実際にはより難しいケースです。__m64型はSIMDベクトルint型(char、short、int、long)をサポートします。Intel APIは__m64を次のように定義しています:

typedef int __m64 __attribute__ ((__vector_size__ (8), __may_alias__));

これはPowerPCターゲットにとって問題があります(GCCでは実際にはサポートされていません)。単一のレジスタで渡すことができるネイティブPowerISA型を使用する方が望ましいです。PowerISAのマスクの下での回転命令は、一般目的レジスタ(GPR)の整数フィールドを簡単に抽出および挿入できます。これにより、MMX整数型はサポートされる要素型の配列の内部ユニオンとして処理できることが示唆されます。64ビットのunsigned long longは、特に64ビットの_si64操作では、パラメータの受け渡しおよび戻り値のための最適な型です。これらの操作は通常、単一のPowerISA命令を生成します。PowerPCの実装では、__m64を次のように定義します:

typedef __attribute__ ((__aligned__ (8))) unsigned long long __m64;

SSE拡張には、_m128との間のコピー/変換操作が含まれており、これにはintからfloatへの変換も含まれます。ただし、これらの場合、floatオペランドは常にSSE(XMM)レジスタに存在し、MMXレジスタには整数値のみが含まれます。POWER8(PowerISA-2.07)は、GPRとVSRの間で直接移動命令を持っています。したがって、これらの転送は通常単一の命令であり、変換はベクトルユニット内で処理できます。

__m64値をベクトルレジスタに転送する場合、浮動小数点操作を行う前に、すべての4つのfloat要素レーンに有効なデータがあることを保証するためにxxsplatd命令を実行する必要があります。これにより、ベクトルの未初期化部分によって引き起こされる余分な浮動小数点例外を回避できます。上位2つのレーンには、GPRに直接転送するか、Store Float Double(stfd)経由で格納するための位置に浮動小数点結果が含まれます。これらの操作はIntrinsic実装の内部で行われ、臨時ベクトルを正しいリトルエンディアン形式に保つ必要はありません。

また、小さな要素サイズと多数の要素(MMX _pi8および_p16型)の場合、64ビットの__m64を要素に分解し、要素計算を行い、単一の__m64値に再構築するために必要なマスクの下での回転命令の数が増える可能性があります。この場合、GPR __m64値をベクトルレジスタに転送し、そこでSIMD操作を行い、次に__m64結果をGPRに戻すことで、より短い命令シーケンスを生成できます。

1.1.2.2. Dealing with AVX and AVX512

AVXはPowerISAおよびELF V2 ABIにとっては少し簡単です。まず、64のベクトルレジスタがあり、スーパースカラーベクトルパイプラインがあり、複数の独立した128ビットベクトル操作を同時に実行できます。第二に、ELF V2 ABIは、より大きな集約をベクトルレジスタで渡すように設計されています:

  • 最大12の資格のあるベクトル引数をv2–v13に渡すことができます。
  • 資格のあるベクトル引数は次に対応します:
    • ベクトルデータ型
    • 同じデータ型の複数のホモジニアス集約のメンバーで、最大8つのベクトルレジスタで渡される。
    • 最大8つの要素からなるホモジニアス浮動小数点またはベクトル集約の戻り値は、関数の最初の入力パラメータが同じ型である場合に使用されるパラメータレジスタに対応する浮動小数点またはベクトルレジスタで返されます。

したがって、ABIは512ビットのベクトルを表す構造体を最大3つ渡し、そのような(512ビット)構造体をVMXレジスタで返すことを可能にします。これをさらに拡張して、パラメータをセーブエリアに溢れさせることができますが、大部分のIntrinsicは2〜3つのオペランドのみを使用するため、これが必要になることはないでしょう。パラメータの受け渡しに必要なベクトルレジスタ以外に、追加の8つの揮発性ベクトルレジスタがあり、アプリケーションはこれをスピルすることなく使用できます。したがって、256ビットまたは512ビットのベクトルに対する大部分のIntrinsic操作は、既存のPowerISAベクトルレジスタ内で処理できます。

AVX 256または512ビットのIntrinsicを複数使用するような大きな関数の場合、揮発性ベクトルレジスタ20を超える場合、コンパイラは非揮発性ベクトルレジスタを割り当て、スタックフレームを割り当てて非揮発性ベクトルレジスタをセーブエリアにスピルすることで、コードの最適化を行います。これにより、最大64のベクトル(32 x 256ビットまたは16 x 512ビット構造体)が最適化のために使用可能になります。

我々のISAおよびABIの特性に基づいて、PowerPCの実装では__vector_size__(32)または(64)を使用しません。その代わりに、2つまたは4つのベクトル(__vector)フィールドの構造体をtypedefします。これにより、これらの大きなデータ型を効率的に処理でき、新しいGCC言語拡張やベクトルビルトインを必要としません。例えば:

/* Internal data types for implementing the AVX in PowerISA intrinsics.  */
typedef struct __v4df
{
  __vector double vd0;
  __vector double vd1;
} __vx4df;

/* The Intel API is flexible enough that we must allow aliasing with other
   vector types, and their scalar components.  */
typedef struct __m256d
{
  __vector double vd0;
  __vector double vd1;
}__attribute__ ((__may_alias__)) __m256d;

これには、操作ごとに128ビットのベクトルチャンクが明示的に参照される場合に異なる構文が必要です。例えば:

extern __inline __m256d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm256_add_pd (__m256d __A, __m256d __B)
{
  __m256d temp;
  temp.vd0 = __A.vd0 + __B.vd0;
  temp.vd1 = __A.vd1 + __B.vd1;
  return (temp);
}

しかし、これは新しい問題を引き起こします。C言語では構造体間の直接キャストを許可していません。Intrinsicインターフェース型が操作に対して正しい型でない場合、これが問題になることがあります。例えば、AVX2整数操作の場合:

/* The Intel API is flexible enough that we must allow aliasing with other
   vector types, and their scalar components.  */
typedef struct __m256i
{
  __vector long long vdi0;
  __vector long long vdi1;
} __m256i;

/* Internal data types for implementing the AVX in PowerISA intrinsics.  */
typedef struct __v16hi
{
  __vector short vhi0;
  __vector short vhi1;
} __v16hi;

AVX2Intrinsic_mm256_add_epi16の場合、64ビットのlong long(__m256i)のベクトルを16ビットのshort(__v16hi)のベクトルにキャストする必要があります。この場合、ポインタ参照キャストを使用する必要があります。例えば:

extern __inline __m256i __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mx256_add_epi16 (__m256i __A, __m256i __B)
{
  __m256i result;
  __v16hi a = *((__v16hi *)&__A);
  __v16hi b = *((__v16hi *)&__B);
  __v16hi c;

  c.vhi0 = a.vhi0 + b.vhi0;
  c.vhi1 = a.vhi1 + b.vhi1;

  result = *((__m256i *)&c);
  return (result);
}

これと関連する例がインライン化されるため、コンパイラはこれが「nopキャスト」であると認識し、追加の命令を生成しないと予想されます。

最終的には、可能な限りGCCのX86Intrinsicヘッダーと同じ型名および定義を使用するべきです。それが不可能な場合は、基礎となるPowerISAハードウェアに最適なマッピングを提供する新しいtypedefを定義できます。

1.1.3. APIの実装方法

嬉しい驚きとして、多くの(少なくとも古いIntel)Intrinsicは、Cベクトル拡張コードやGCCターゲット固有のビルトインへの簡単なマッピングで直接実装されています。

1.1.3.1. 一部の簡単な例

例えば、ベクトルダブルスプラットは次のようになります:

/* Create a vector with both elements equal to F.  */
extern __inline __m128d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_set1_pd (double __F)
{
  return __extension__ (__m128d){ __F, __F };
}

別の例:

extern __inline __m128d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_add_pd (__m128d __A, __m128d __B)
{
  return (__m128d) ((__v2df)__A + (__v2df)__B);
}

上記の例では、操作のために__v2dfにキャストしています。__m128dと__v2dfの両方がベクトルダブルですが、__v2dfには__may_alias__属性がありません。さらに別の例:

extern __inline __m128i __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_mullo_epi16 (__m128i __A, __m128i __B)
{
  return (__m128i) ((__v8hu)__A * (__v8hu)__B);
}

この場合、コンパイラが意図された操作のために正しいコードを生成するためには、キャストが必要です。パラメータと結果は__m128iというジェネリックインターフェース型で、これはベクトルlong longに__may_alias__属性を持つものです。しかし、操作はベクトルの低乗算で、要素型はunsigned shortです。そのため、__may_alias__属性を削除するためのキャストが必要であり、さらに操作のために正しい型(__v8huまたはベクトルunsigned short)にキャストする必要があります。

これら(および類似の)ソーススニペットをPPC64LE実装に変更せずにコピーすることに成功しました。もちろん、関連する型が定義され、互換性のある属性を持っていることが前提です。

1.1.3.2. 追加の属性について

いくつかの特別な属性に気付いたかもしれません:

__gnu_inline__

この属性は、インラインキーワードとともに宣言された関数に使用する必要があります。これは、C99またはgnu99モードでコンパイルしている場合でも、関数をgnu90モードで定義されたかのようにGCCに指示します。

関数がexternと宣言されている場合、この関数の定義はインライン化にのみ使用されます。関数がスタンドアロン関数としてコンパイルされることはありません。このような場合、そのアドレスを明示的に取ると、そのアドレスは外部参照になります。この方法を使用するには、ヘッダーファイルにこの属性を持つ関数定義を配置し、ライブラリファイルにはexternなしで別の関数定義を配置します。ヘッダーファイルの定義により、関数へのほとんどの呼び出しがインライン化されます。

__always_inline__

一般的に、最適化が指定されていない限り、関数はインライン化されません。インラインと宣言された関数にこの属性を付けると、インライン化に適用される他の制限に関係なく関数がインライン化されます。このような関数のインライン化に失敗すると、エラーとして診断されます。

__artificial__

この属性は、可能であればデバッグ時に単位として表示されるべき小さなインラインラッパーに役立ちます。デバッグ情報の形式によっては、関数を人工的にマークするか、インライン化された本体内のすべての命令に対して呼び出し元の位置を使用します。

__extension__

... -pedanticおよび他のオプションは、多くのGNU C拡張に対して警告を発します。この属性を使用することで、1つの式内でそのような警告を防ぐことができます。

これまで、これらの属性を変更せずに使用してきました。

多くのIntrinsicは、IntelIntrinsicを1つまたは複数のターゲット固有のGCCビルトインにマッピングしています。例えば:

/* Load two DPFP values from P.  The address must be 16-byte aligned.  */
extern __inline __m128d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_load_pd (double const *__P)
{
  return *(__m128d *)__P;
}
/* Load two DPFP values from P.  The address need not be 16-byte aligned.  */
extern __inline __m128d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_loadu_pd (double const *__P)
{
  return __builtin_ia32_loadupd (__P);
}

最初のIntrinsic(_mm_load_pd)は、Cベクトルポインタ参照として実装されていますが、コメントからわかるように、コンパイラが16バイトアラインメントを要求するmovapd命令を使用すると仮定しています(アラインされていない場合は一般保護例外を発生させます)。これは少なくとも一部のIntelプロセッサにとってベクトルをアラインさせることがパフォーマンス上の利点があることを示唆しています。第二のIntrinsicは、アラインされていない参照を処理するmovupd命令を生成するGCCビルトイン__builtin_ia32_loadupdを使用しています。

PowerおよびPPC64LEに対する逆の仮定が適用されます。GCCはデフォルトでVSX lxvd2x / xxswapd命令シーケンスを生成し、アラインされていない参照を許可します。PowerISAのアラインされたベクトルアクセスに相当するものはVMX lvx命令とvec_ldビルトインで、これはクアッドワードアラインアクセスを強制します(有効アドレスの下位4ビットを無視します)。lvx命令はアラインメント例外を発生させませんが、IntelIntrinsicの実装の一部としては発生させるべきかもしれません。このため、期待される結果を得るためにPowerISA VMX/VSXビルトインを使用する必要があります。

現在のプロトタイプでは、次のように定義されています:

/* Load two DPFP values from P.  The address must be 16-byte aligned.  */
extern __inline __m128d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_load_pd (double const *__P)
{
  assert(((unsigned long)__P & 0xfUL) == 0UL);
  return ((__m128d)vec_ld(0, (__v16qu*)__P));
}

/* Load two DPFP values from P.  The address need not be 16-byte aligned.  */
extern __inline __m128d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_loadu_pd (double const *__P)
{
  return (vec_vsx_ld(0, __P));
}

アラインされたロードIntrinsicは、アラインメントをチェックするassertを追加し(Intelのセマンティクスに一致するように)、GCCビルトインvec_ld(lvxを生成)を使用します。assertは追加のコードを生成しますが、コンパイル時にNDEBUGを定義することでこれを排除できます。アラインされていないロードIntrinsicは、GCCビルトインvec_vsx_ld(PPC64LEの場合、POWER8のためにlxvd2x / xxswapdを生成し、POWER9の場合にはlxvまたはlxvxに簡略化される)を使用します。同様に、__mm_store_pd / __mm_storeu_pdもvec_stおよびvec_vsx_stを使用します。これらの概念は、ベクトルfloatおよびベクトルintのロード/ストアIntrinsicにも適用されます。

1.1.3.3. どうやってこれを見つけたのか?

次の質問は、上記の詳細をどこで見つけたのかということです。GCCの__builtin_ia32_loadupdに関するドキュメントには、最小限の情報(ビルトイン名、パラメータ、および戻り値の型)しか提供されていません。非常に有益ではありません。

IntelのIntrinsicの説明を調べる方が有益です。Intrinsic名をGoogleで検索するか、Intel Intrinsic Guideを使用してIntrinsic名を調べると良いでしょう。Intrinsicガイドはインタラクティブであり、Intel(チップ)技術およびテキストベースの検索機能を提供します。Intrinsic名をクリックすると、基礎となる命令名、テキスト説明、操作擬似コード、および一部の場合にはパフォーマンス情報(レイテンシーおよびスループット)を含む概要が表示されます。

重要なのは、Intrinsicの説明(オペランドフィールドおよび型、結果のために更新されるフィールド)と基礎となるIntel命令の説明を取得することです。Intrinsicガイドが明確でない場合、「Intel® 64 and IA-32 Architectures Software Developer’s Manual」で命令の詳細を調べることができます。

PowerISAベクトル施設に関する情報は、PowerISAバージョン2.07B(POWER8用)および3.0(POWER9用)マニュアルの第1巻、第6章ベクトル施設および第7章ベクトルスカラ浮動小数点操作に記載されています。また、OpenPOWER ELF V2アプリケーションバイナリインターフェース(ABI)ドキュメントの第6章ベクトルプログラミングインターフェースおよび付録Aの事前定義された関数も参考になります。

もう一つの有用なドキュメントは、元のAltivec技術プログラマーズインターフェースマニュアルです。これは使いやすい構造を持ち、多くの有用な図があります。ただし、PIMは最近のPowerISA(power7、power8、およびpower9)の拡張をカバーしていません。

1.1.3.4. 他のIntrinsicを使用して実装された例

一部のIntrinsic実装は他のIntrinsicを使用して定義されています。例えば:

/* Create a vector with element [0] as F and the rest zero.  */
extern __inline __m128d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_set_sd (double __F)
{
  return __extension__ (__m128d){ __F, 0.0 };
}

/* Create a vector with element [0] as *P and the rest zero.  */
extern __inline __m128d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_load_sd (double const *__P)
{
  return _mm_set_sd (*__P);
}

SSEスカラー操作に特有のもので、SSE XMMレジスタの一部(4分の1または半分)を使用し、残りを変更せずに残す(またはゼロに設定する)という概念は、PowerISAコードで複雑な(非最適)コードを生成することがあります。この場合、_mm_load_sdは参照解除されたdouble値を_mm_set_sdに渡し、Cベクトル初期化子構文を使用して、そのdoubleスカラー値をスカラー0.0定数と組み合わせてベクトルダブルにします。

このようなコードはそのままPPC64LEで動作するはずですが、生成されたコードを確認して合理的かどうかを評価する必要があります。この場合、コードはひどくはありません(ロードダブルスプラット、0.0sを生成するベクトルxor、次に__Fと0.0を組み合わせるためのxxmrghd)。他の例では非最適コードが生成される場合があり、PowerISAスカラーまたはベクトルコード(GCC PowerPC AltiVecビルトイン関数またはインラインアセンブリ)に書き直す価値があるかもしれません。

注意

可能であれば、既存のCコードを使用してみてください。しかし、生成されたコードを確認してください。生成されたコードがひどい場合は、PowerISA固有の同等物に書き直す価値があるかもしれません。MMXやSSEスカラーIntrinsicを多用するコードについては、標準のCスカラー型を使用して書き直し、GCCコンパイラに詳細を処理させる方が良いでしょう(セクション2.1、「推奨される方法」を参照)。

2. これをどう使うのか?

作業仮定は、現在のGCCヘッダーの./gcc/config/i386/から始め、それをPowerISAに変換し、./gcc/config/rs6000/に追加することです。既存のヘッダー構造を複製し、既存のヘッダーファイルとIntrinsic名を保持することを前提としています。これにより、./gcc/testsuite/gcc.target/i386から既存のDejaGNUテストケースを再利用し、必要に応じてPOWERターゲット用に変更し、./gcc/testsuite/gcc.target/powerpcに追加することができます。

ヘッダー/Intrinsicとテストケースのポーティング順序は柔軟に対応できます。これは、顧客のニーズと内部依存関係の解決に基づくべきです。つまり、古いものから新しいものへ/ボトムアップ(MMX、SSE、SSE2、... AVX512)です。

2.1. 推奨される方法

上記のアプローチは、ターゲット固有のヘッダーをポーティングするためのものであり、特にパフォーマンスに敏感なホットスポット関数に適用されます。しかし、これは唯一のアプローチではありません。Intrinsicは一般的にC/C++ソースコード内で直接使用されます。X86Intrinsicの使用を、コード内のターゲット依存部分として認識し、それに対して抽象化/ポーティングレイヤーを提供することをお勧めします。

2.2. 準備する

Intrinsicの使用は、通常、ベクトル化されたパフォーマンスの重要な部分であり、一般に最適化されたコードの重要な部分です。このセクションでは、GCCベクトル拡張、IntelIntrinsic、およびPowerISAベクトル施設に関する基礎的な概念を説明します。

2.2.1. GCC Vector Extensions

GCCは、CおよびC++言語のベクトル拡張をサポートしています。これにより、ベクトル型とベクトル操作を使用したプログラムを書くことができます。ベクトル型は、基本的な整数および浮動小数点型に基づいており、各型の複数の要素を含むことができます。例えば、次のようにベクトル型を定義できます:

typedef int v4si __attribute__ ((vector_size (16)));
typedef float v4sf __attribute__ ((vector_size (16)));

これにより、4つの整数または4つの浮動小数点数を含むベクトルが作成されます。ベクトル型に対して通常の算術および比較操作を使用できます。

v4si a, b, c;
a = (v4si) {1, 2, 3, 4};
b = (v4si) {5, 6, 7, 8};
c = a + b;  // ベクトルの要素ごとに加算される

この例では、各ベクトルの対応する要素が加算され、新しいベクトルcが作成されます。

2.2.2. Intel Intrinsic関数

IntelIntrinsicは、ベクトル型および操作を提供するヘッダーファイルのセットです。これらのヘッダーには、特定のハードウェア機能を利用するための関数が含まれています。Intrinsic関数は通常、コンパイラによって対応するアセンブリ命令に変換されます。

2.2.2.1. パックド vs スカラ型intrinsics

Intrinsicには、ベクトル型(パックド)およびスカラ型の両方が存在します。パックドIntrinsicはベクトル全体を操作し、スカラIntrinsicは個々の要素を操作します。例えば、_mm_add_psは4つの浮動小数点数のベクトルを加算し、_mm_add_ssは単一の浮動小数点数を加算します。

2.2.2.2. vec_not か not

ベクトル操作において、否定操作(ビットごとのNOT)が必要になる場合があります。IntelIntrinsicでは、対応する操作が提供されている場合とされていない場合があります。必要に応じて、GCCのベクトル拡張を使用してビットごとのNOT操作を実装できます。

2.2.2.3. レーンの交差

ベクトル操作において、異なるレーン間でデータを操作する必要がある場合があります。これは、例えば、横方向に加算を行う場合です。IntelIntrinsicでは、レーンをまたいだ操作をサポートするための関数が提供されています。

2.2.3. PowerISAベクトル機能

PowerISAベクトル機能は、128ビットベクトルレジスタおよび関連する命令セットを提供します。これは、VMXおよびVSX命令セットを含み、ベクトル整数および浮動小数点操作をサポートします。

2.2.3.1. PowerISA

PowerISA(Power Instruction Set Architecture)は、IBMのPowerプロセッサ用の命令セットアーキテクチャです。PowerISAは、高度なベクトルおよび浮動小数点演算をサポートし、複雑な計算を効率的に実行できます。

2.2.3.2. PowerISA ベクトルIntrinsics

PowerISAベクトルIntrinsicは、GCCのベクトル拡張を基にしたインライン関数であり、VMXおよびVSX命令セットに対応しています。これらのIntrinsicは、ベクトル型および操作を提供し、Powerプロセッサのベクトル機能を活用するために使用されます。

2.2.3.3. ベクトル要素のサイズと型の変更方法

ベクトル要素のサイズおよび型を変更する場合、適切な変換操作を使用する必要があります。GCCのベクトル拡張では、要素サイズおよび型を変更するための関数が提供されています。例えば、整数ベクトルを浮動小数点ベクトルに変換する場合は、対応するキャスト操作を使用できます。

2.2.4. さらにいくつかのIntrinsicの例

以下に、いくつかの具体的なIntrinsicの例を示します。

/* Create a vector with both elements equal to F.  */
extern __inline __m128d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_set1_pd (double __F)
{
  return __extension__ (__m128d){ __F, __F };
}

/* Add two DPFP vectors.  */
extern __inline __m128d __attribute__((__gnu_inline__, __always_inline__, __artificial__))
_mm_add_pd (__m128d __A, __m128d __B)
{
  return (__m128d) ((__v2df)__A + (__v2df)__B);
}

これらのIntrinsicは、ベクトル要素を操作するための基本的な機能を提供します。適切な型キャストおよびGCCのベクトル拡張を使用することで、PowerISAベクトルIntrinsicの同等の機能を実装できます。

3. 本質的な違い

このセクションでは、IntelIntrinsicとPowerISAベクトル施設の間の本質的な違いについて説明します。特に、浮動小数点例外、浮動小数点丸めモード、およびパフォーマンスに関連する違いについて詳述します。

3.1. 浮動小数点例外

Intelアーキテクチャは、浮動小数点例外をデフォルトでマスクします。これは、例外が発生してもプログラムの実行が続行されることを意味します。必要に応じて、プログラムは例外マスクを解除し、特定の浮動小数点操作に対する例外処理を有効にできます。一般的な浮動小数点例外には、ゼロによる除算、オーバーフロー、アンダーフロー、不正な操作、および非正規化されたオペランドの使用が含まれます。

PowerISAは、同様の浮動小数点例外をサポートしています。ただし、例外処理の方法は異なる場合があります。PowerISAでは、特定の浮動小数点操作に対する例外処理を有効にするために、FSCR(Floating-Point Status and Control Register)レジスタを使用します。このレジスタを適切に設定することで、特定の例外が発生したときに割り込みが発生し、例外処理ルーチンが呼び出されるようにできます。

3.2. 浮動小数点丸めモード

IntelアーキテクチャおよびPowerISAの両方は、さまざまな浮動小数点丸めモードをサポートしています。一般的な丸めモードには、最近接丸め(偶数への丸め)、ゼロへの丸め、負の無限大への丸め、および正の無限大への丸めがあります。

Intelアーキテクチャでは、MXCSRレジスタを使用して現在の丸めモードを設定および取得します。次の例は、MXCSRレジスタを使用して丸めモードを設定する方法を示しています:

unsigned int mxcsr;
__asm__ __volatile__ ("stmxcsr %0" : "=m" (mxcsr));  // MXCSRレジスタの値を取得
mxcsr = (mxcsr & ~0x6000) | 0x2000;  // 丸めモードを最近接丸めに設定
__asm__ __volatile__ ("ldmxcsr %0" : : "m" (mx

csr));  // MXCSRレジスタの値を設定

PowerISAでは、FPSCR(Floating-Point Status and Control Register)レジスタを使用して現在の丸めモードを設定および取得します。次の例は、FPSCRレジスタを使用して丸めモードを設定する方法を示しています:

unsigned int fpscr;
__asm__ __volatile__ ("mffs %0" : "=f" (fpscr));  // FPSCRレジスタの値を取得
fpscr = (fpscr & ~0x03) | 0x00;  // 丸めモードを最近接丸めに設定
__asm__ __volatile__ ("mtfsf 255, %0" : : "f" (fpscr));  // FPSCRレジスタの値を設定

これらの例では、レジスタの内容を操作して丸めモードを設定しています。浮動小数点操作の結果が異なる丸めモードで異なることがあるため、特定の丸めモードが必要な場合は、適切に設定する必要があります。

3.3. パフォーマンス

IntelIntrinsicとPowerISAベクトル施設のパフォーマンスは、さまざまな要因によって異なります。特に、レジスタの数、命令セットの機能、およびハードウェアアーキテクチャの違いがパフォーマンスに影響を与えます。

3.3.1. SSE floatおよびdoubleスカラーを使用する

IntelのSSE(Streaming SIMD Extensions)は、ベクトル浮動小数点操作をサポートします。SSEIntrinsicは、通常、ベクトルレジスタ内の4つのfloatまたは2つのdouble要素を操作します。次の例は、SSEIntrinsicを使用して2つのベクトルの要素ごとに加算する方法を示しています:

__m128 a = _mm_set_ps(1.0, 2.0, 3.0, 4.0);
__m128 b = _mm_set_ps(5.0, 6.0, 7.0, 8.0);
__m128 c = _mm_add_ps(a, b);  // a + b を計算

この例では、_mm_set_ps関数を使用してベクトルaおよびbを設定し、_mm_add_ps関数を使用してそれらを加算しています。

PowerISAは、同様にベクトル浮動小数点操作をサポートします。次の例は、PowerISAベクトルIntrinsicを使用して同じ操作を実行する方法を示しています:

vector float a = {1.0, 2.0, 3.0, 4.0};
vector float b = {5.0, 6.0, 7.0, 8.0};
vector float c = vec_add(a, b);  // a + b を計算

この例では、vector float型を使用してベクトルaおよびbを設定し、vec_add関数を使用してそれらを加算しています。

3.3.2. MMXIntrinsicを使用する

IntelのMMX(MultiMedia Extensions)は、ベクトル整数操作をサポートします。MMXIntrinsicは、通常、64ビットベクトルレジスタ内の8つの8ビット整数、4つの16ビット整数、または2つの32ビット整数を操作します。次の例は、MMXIntrinsicを使用して2つのベクトルの要素ごとに加算する方法を示しています:

__m64 a = _mm_set_pi32(1, 2);
__m64 b = _mm_set_pi32(3, 4);
__m64 c = _mm_add_pi32(a, b);  // a + b を計算

この例では、_mm_set_pi32関数を使用してベクトルaおよびbを設定し、_mm_add_pi32関数を使用してそれらを加算しています。

PowerISAは、同様にベクトル整数操作をサポートします。次の例は、PowerISAベクトルIntrinsicを使用して同じ操作を実行する方法を示しています:

vector signed int a = {1, 2};
vector signed int b = {3, 4};
vector signed int c = vec_add(a, b);  // a + b を計算

この例では、vector signed int型を使用してベクトルaおよびbを設定し、vec_add関数を使用してそれらを加算しています。


付録A. ドキュメントの参照

このセクションでは、参考文献および追加のドキュメントについて説明します。詳細な技術情報および仕様については、以下のドキュメントを参照してください:

  • Intel® 64 and IA-32 Architectures Software Developer’s Manual
  • PowerISA™ Version 2.07B
  • PowerISA™ Version 3.0
  • OpenPOWER ELF V2 ABI Specification
  • GCC documentation on vector extensions

付録B. Intel Intrinsic suffixes

IntelIntrinsicの接尾辞には、操作の型およびサイズを示すための情報が含まれています。以下に一般的な接尾辞の例を示します:

  • _ps:単精度浮動小数点数(packed single-precision floating-point)
  • _pd:倍精度浮動小数点数(packed double-precision floating-point)
  • _ss:単精度浮動小数点数(scalar single-precision floating-point)
  • _sd:倍精度浮動小数点数(scalar double-precision floating-point)
  • _pi8:8ビット整数(packed 8-bit integers)
  • _pi16:16ビット整数(packed 16-bit integers)
  • _pi32:32ビット整数(packed 32-bit integers)
  • _epi8:符号付き8ビット整数(extended packed 8-bit integers)
  • _epi16:符号付き16ビット整数(extended packed 16-bit integers)
  • _epi32:符号付き32ビット整数(extended packed 32-bit integers)

付録C. OpenPOWER Foundationの概要

OpenPOWER Foundationは、IBMのPowerアーキテクチャに基づいたオープンハードウェアおよびソフトウェアエコシステムの開発を促進するための業界コンソーシアムです。OpenPOWER Foundationのメンバーは、Powerプロセッサ、システム、アクセラレータ、ソフトウェア、ファームウェア、およびその他の技術を開発し、共有しています。詳細については、OpenPOWER Foundationの公式ウェブサイトを参照してください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0