FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

RISC-V Advanced Platform Interrupt Controller (APLIC) の概観

RISC-VのAdvanced Platform Interrupt Controller (APLIC) について勉強してみようと思う。

まずは以下のブログを読んで概観を掴もう。

blog.stephenmarz.com

APLICの概観

APLICはSiFiveのPLICを発展させたもので、PLICとの違いはメッセージによる割り込みの送信をサポートしていることである。 PLICの目的はハードウェア割り込み信号の集約と優先順位付け、そして割り込み通知信号の送信だった。 割り込み送信先はHARTで、HARTはClaimレジスタと呼ばれるPLICレジスタを読むことによって何が通知されたかを判断することができる。

APLICは、Incoming MSI Controller(IMSIC)と組み合わせることによって、APLICは他のハードウェアデバイスと同じようにメッセージを送信する能力を持つ。

APLICのメモリマップ

注意しなければならないのは、APLICとPLICの間に互換性はないということである。

struct Aplic {
    pub domaincfg: u32,           // Domain CSR that controls how this APLIC functions
    pub sourcecfg: [u32; 1023],   // Source configuration for 1023 interrupts
    _reserved1: [u8; 0xBC0],

    pub mmsiaddrcfg: u32,         // Machine-level MSI address (for APLIC to write MSIs)
    pub mmsiaddrcfgh: u32,
    pub smsiaddrcfg: u32,         // Supervisor-level MSI address
    pub smsiaddrcfgh: u32,
    _reserved2: [u8; 0x30],

    pub setip: [u32; 32],         // Bitset to set pending interrupts (32 IRQS per element)
    _reserved3: [u8; 92],

    pub setipnum: u32,            // Sets a pending interrupt by number
    _reserved4: [u8; 0x20],

    pub clrip: [u32; 32],         // Bitset to clear pending interrupts (opposite of setip)
    _reserved5: [u8; 92],

    pub clripnum: u32,            // Clears a pending interrupt by number
    _reserved6: [u8; 32],

    pub setie: [u32; 32],         // Bitset to enable interrupts
    _reserved7: [u8; 92],

    pub setienum: u32,            // Enable an interrupt by number
    _reserved8: [u8; 32],

    pub clrie: [u32; 32],         // Bitset to disable interrupts (opposite of setie)
    _reserved9: [u8; 92],

    pub clrienum: u32,            // Disable an interrupt by number
    _reserved10: [u8; 32],

    pub setipnum_le: u32,         // Set an interrupt pending by number always little end first
    pub setipnum_be: u32,         // Set an interrupt pending by number always big end first
    _reserved11: [u8; 4088],

    pub genmsi: u32,              // Used to generate MSIs
    pub target: [u32; 1023],      // Target control per interrupt
}

ドメイン構成レジスタ(domaincfg)

  • 割り込み許可ビット(Interrupt Enable: IE)は、APLICが割り込みを送信できるようにする(1 = 有効、0 = 無効)

    • これは割り込みが必ず通知されることを意味するのではなく、pendingビットをトリガすることによってAPLICが割り込みを送信できることを意味するだけである。
  • 配信モードビット(delivery mode): APLICは旧PLICのように通常の割り込みを送信するか、割り込みをメッセージ(MSI)として送信するかを設定する。

    • DM ビットが 0 に設定されると、古い APLIC のように direct 割り込みを送信する
    • DMビットが1に設定されると、代わりにMSIを送信する。
  • ビッグエンディアンビット(Big Endian)により、APLICはビッグエンディアン(BE=1)またはリトルエンディアン(BE=0)でメッセージを書き込むことができる。

    • BE ビットはマルチバイト・ドメイン・コンフィギュレーション・レジスタの順序にも影響する。

最上位バイトは、バイト順マークのような役割を果たすために、意図的に0x80に設定されている。

ソース設定レジスタ(sourcecfg)

すべての割り込みに対して 1 つの sourcecfg レジスタが定義されている。

  • delegateビット(ビット10)は、与えられた割り込みが委譲されているかどうかを判断するために読むことができます。 このビットは読み書き可能である。 このフィールドに1を書き込むと、子ドメインに委譲される。 特定のソースに子ドメインがない場合、このビットは常に0を読み取る。
  • ソース・モードビット(ビット2:0) は、委譲されていない割り込みについて、割り込みがどのようにトリガされるかを制御します。
  • 割り込みが委譲されている(D=1)場合、ビット9:0はそれが委譲された子のインデックスを記述します。
  • 割り込みが委譲されていない場合(D=0)、各割り込みソースは、以下のいずれかによって「保留中」割り込みをトリガするように構成することができます。

LLVM18を久しぶりにビルドしようとしたら失敗する問題の解析 (3. Baremetalでのコンパイル確認)

LLVM18のRISC-V Targetでのビルド方法が分からなくてずっと悩んでいたのだが、とりあえず以下の方法ならば行けるということが分かってきた。

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

LLVM18でC++のコンパイルを簡単なコードで確認しておく:

  • main.cc
#include <iostream>

int main()
{
  std::cout << "Hello World" << std::endl;
}

Clang++のバージョンは以下で試行した:

clang version 18.1.8 (https://github.com/llvm/llvm-project.git 3b5b5c1ec4a3095ab096dd780e84d7ab81f3d7ff)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /riscv//bin
clang++ -Wall --target=riscv64-unknown-elf  main.cc -o main
$ spike pk main
Hello World

正しく動いているようだ。

LLVM18を久しぶりにビルドしようとしたら失敗する問題の解析 (2. Baremetal版のコンパイル方法)

LLVM18のRISC-V Targetでのビルド方法が分からなくてずっと悩んでいたのだが、とりあえず以下の方法ならば行けるということが分かってきた。

msyksphinz.hatenablog.com

$ cd build
$ cmake -G Ninja \
    ../llvm \
    -DCMAKE_BUILD_TYPE=Release \
    -DLLVM_ENABLE_PROJECTS="clang;lld" \
    -DLLVM_TARGETS_TO_BUILD="RISCV" \
    -DLLVM_DEFAULT_TARGET_TRIPLE="riscv64-unknown-elf" \
    -DDEFAULT_SYSROOT="${RISCV}/riscv64-unknown-elf" \
    -DGCC_INSTALL_PREFIX="${RISCV}"
$ ninja

とりあえずのポイントは、-DLLVM_ENABLE_RUNTIMES="compiler-rt;libcxx;libcxxabi;libunwind" を全部除去したことだ。これでいろいろプログラムがコンパイルできなくなってしまっては困るが、とりあえずLLVMのビルドはうまくいく。

ちょっとこれでしばらく運用してみよう。問題があれば再度解析する。

オープンソース・アウトオブオーダCPU NaxRiscvを概観する (6. SpinalHDLで記述されたRVC Decompressor)

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

もうちょっとなんとかSpinal HDLのわけわからん記述が読めるようになりたい: とりあえずRVC Alignerの部分で何とか理解できる部分はないだろうか。

Aligner Pluginをもうちょっと腰を据えて解析してみたいと思う。

RVC付きのXLEN=64でNaxRiscvを生成し直そうと思う。

  • NaxRiscv/src/main/scala/naxriscv/Gen.scala
// ramstyle = "MLAB, no_rw_check"
object Gen64 extends App{
  LutInputs.set(6)
  def plugins = {
    val l = Config.plugins(
      sideChannels = false, //WARNING, FOR TEST PURPOSES ONLY, turn to false for real stuff <3
      xlen = 64,
      withRdTime = false,
      aluCount    = 2,
      decodeCount = 2,
      withRvc = true,
      withDebug = false,
      withEmbeddedJtagTap = false,
      debugTriggers = 4,
      withFloat = false,
      withDouble = false,
      lqSize = 16,
      sqSize = 16,
      asic = false
    )
    l.foreach{
      case p : EmbeddedJtagPlugin => p.debugCd.load(ClockDomain.current.copy(reset = Bool().setName("debug_reset")))
      case _ =>
    }
//    Tweek.euWritebackAt(l, "ALU0", 1)
//    Tweek.euWritebackAt(l, "ALU1", 1)
    l
  }

キャッシュブロックが128ビットの場合、16ビットのRVCを検出すべき単位は以下の図のように8個に分割される。

NaxRiscvの実装ではキャッシュ・ブロックが64ビットなので、4つのブロック分割されるはずだ。 キャッシュ・ブロックのビット幅/16ビットなので、NaxRiscvではSLICE_COUNT=4となるはずだ。

このときのslicesの作り方がまた良く分からないのだが、dataWORDbuffer.data の concatenate、そしてこれをsubdivideInによりSLICE_WIDTHビットごとに切り分ける。 つまり、dataSLICE_WIDTH毎のSLICE_COUNT*2個のデータ要素に分けられる。

    val slices = new Area {
      val data = (WORD ## buffer.data).subdivideIn(SLICE_WIDTH bits)
      var carry = (isInputValid ? input(MASK_FRONT) | B(0)) ## buffer.mask //Which slice have valid data
      var remains = CombInit(carry)
      var used = B(0, SLICE_COUNT*2 bits)
    }
  wire       [127:0]  _zz_AlignerPlugin_logic_slices_data_0;
  wire       [15:0]   AlignerPlugin_logic_slices_data_0;
  wire       [15:0]   AlignerPlugin_logic_slices_data_1;
  wire       [15:0]   AlignerPlugin_logic_slices_data_2;
  wire       [15:0]   AlignerPlugin_logic_slices_data_3;
  wire       [15:0]   AlignerPlugin_logic_slices_data_4;
  wire       [15:0]   AlignerPlugin_logic_slices_data_5;
  wire       [15:0]   AlignerPlugin_logic_slices_data_6;
  wire       [15:0]   AlignerPlugin_logic_slices_data_7;

  assign _zz_AlignerPlugin_logic_slices_data_0 = {AlignerPlugin_setup_s2m_Fetch_WORD,AlignerPlugin_logic_buffer_data};
  assign AlignerPlugin_logic_slices_data_0 = _zz_AlignerPlugin_logic_slices_data_0[15 : 0];
  assign AlignerPlugin_logic_slices_data_1 = _zz_AlignerPlugin_logic_slices_data_0[31 : 16];
  assign AlignerPlugin_logic_slices_data_2 = _zz_AlignerPlugin_logic_slices_data_0[47 : 32];
  assign AlignerPlugin_logic_slices_data_3 = _zz_AlignerPlugin_logic_slices_data_0[63 : 48];
  assign AlignerPlugin_logic_slices_data_4 = _zz_AlignerPlugin_logic_slices_data_0[79 : 64];
  assign AlignerPlugin_logic_slices_data_5 = _zz_AlignerPlugin_logic_slices_data_0[95 : 80];
  assign AlignerPlugin_logic_slices_data_6 = _zz_AlignerPlugin_logic_slices_data_0[111 : 96];
  assign AlignerPlugin_logic_slices_data_7 = _zz_AlignerPlugin_logic_slices_data_0[127 : 112];

これらをデコーダを用いて展開していく。

    val decoders = for (i <- 0 until SLICE_COUNT * 2) yield new Area {
      val rvc = RVC.get generate slices.data(i)(1 downto 0) =/= 3

      val usage = if(RVC) {
        def mask16 = B(1 << i, SLICE_COUNT*2 bits)
        def mask32 = B(3 << i, SLICE_COUNT*2 bits)
        if(i == SLICE_COUNT*2 - 1) mask16 else rvc ? mask16 | mask32

RVCが有効で、当該16ビットがrvcブロックとして認識されればmask16でマスクし、そうでなければ32ビット命令なので2ブロックぶんとしてマスクする。

  assign AlignerPlugin_logic_decoders_0_usage = (AlignerPlugin_logic_decoders_0_rvc ? 8'h01 : 8'h03);
  assign AlignerPlugin_logic_decoders_1_usage = (AlignerPlugin_logic_decoders_1_rvc ? 8'h02 : 8'h06);
  assign AlignerPlugin_logic_decoders_2_usage = (AlignerPlugin_logic_decoders_2_rvc ? 8'h04 : 8'h0c);
  assign AlignerPlugin_logic_decoders_3_usage = (AlignerPlugin_logic_decoders_3_rvc ? 8'h08 : 8'h18);
  assign AlignerPlugin_logic_decoders_4_usage = (AlignerPlugin_logic_decoders_4_rvc ? 8'h10 : 8'h30);
  assign AlignerPlugin_logic_decoders_5_usage = (AlignerPlugin_logic_decoders_5_rvc ? 8'h20 : 8'h60);
  assign AlignerPlugin_logic_decoders_6_usage = (AlignerPlugin_logic_decoders_6_rvc ? 8'h40 : 8'hc0);
  assign AlignerPlugin_logic_decoders_7_usage = 8'h80;

notEnoughDataは、2キャッシュライン分のデータにおいて、

  • 最上位がRVI(=32ビット)場合は、キャッシュラインを跨ぐのでデータが足りない。
  • 真ん中のブロックの場合は、RVI(=32ビット)かつ上位キャッシュラインが無効

というときに成立する、というものだと思う。

      val notEnoughData = RVC.get match {
        case true =>
          if(i == SLICE_COUNT * 2 - 1)
            !rvc
          else if(i == SLICE_COUNT - 1)
            !rvc && (!MASK_FRONT.lsb || !isInputValid)
          else
            False
        case false => False
      }

次に pastPredictionは2キャッシュラインのうち上位のキャッシュラインにおいて、WORD_BRANCH_SLICEよりも大きさブロック位置であれば有効となる。これはどういうことだ?多分分岐命令の位置を予測しているのかな?

      val pastPrediction = if(i <= SLICE_COUNT) False else WORD_BRANCH_VALID && U(i - SLICE_COUNT) > WORD_BRANCH_SLICE //TODO may use MASK_BACK instead for timings ?
      val usable = !notEnoughData && !pastPrediction
    }
  assign FetchPlugin_stages_1_BtbPlugin_logic_ENTRY_slice = _zz_FetchPlugin_stages_1_BtbPlugin_logic_ENTRY_hash[17 : 16];

  assign FetchPlugin_stages_1_Prediction_WORD_BRANCH_SLICE = FetchPlugin_stages_1_BtbPlugin_logic_ENTRY_slice;

オープンソース・アウトオブオーダCPU NaxRiscvを概観する (7. RVCサポートのNaxRiscvをRTLシミュレーションする)

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

ちょっとRVC付きのNaxRiscvについてRTLシミュレーションの流し方をまとめておく。

  • ビルド対象について、withRvcを有効化しておく。
diff --git a/src/main/scala/naxriscv/Gen.scala b/src/main/scala/naxriscv/Gen.scala
index 031ce67..b33862e 100644
--- a/src/main/scala/naxriscv/Gen.scala
+++ b/src/main/scala/naxriscv/Gen.scala
@@ -529,7 +529,7 @@ object Gen64 extends App{
       withRdTime = false,
       aluCount    = 2,
       decodeCount = 2,
-      withRvc = false,
+      withRvc = true,
       withDebug = false,
       withEmbeddedJtagTap = false,
       debugTriggers = 4,
$ sbt "runMain naxriscv.Gen64"
$ cd $NAXRISCV/src/test/cpp/naxriscv
$ make compile

RTLシミュレーションの方法:

$ ./obj_dir/VNaxRiscv --trace --trace-ref --name coremark --load-elf ../../../../ext/NaxSoftware/baremetal/coremark/build/rv64imac/coremark.elf --pass-symbol pass
2K performance run parameters for coremark.
CoreMark Size    : 666
Total ticks      : 2166532
Total time (secs): 2166532.000000
Iterations/Sec   : 0.000005
Iterations       : 10
Compiler version : GCC11.1.0
Compiler flags   : -DPERFORMANCE_RUN=1  -march=rv64imac -mabi=lp64 -mcmodel=medany -Wno-pointer-to-int-cast -Wno-int-to-pointer-cast -I../driver -O3 -fno-common -funroll-loops -finline-functions -falign-functions=16 -falign-jumps=4 -falign-loops=4 -finline-limit=1000 -fno-if-conversion2 -fselective-scheduling -fno-crossjumping -freorder-blocks-and-partition -DCORE_DEBUG=0  -lgcc -lc -nostartfiles -ffreestanding -Wl,-Bstatic,-T,../common/app.ld,-Map,coremark.map,--print-memory-usage
Memory location  : STACK
seedcrc          : 0xe9f5
[0]crclist       : 0xe714
[0]crcmatrix     : 0x1fd7
[0]crcstate      : 0x8e3a
[0]crcfinal      : 0xfcaf
Correct operation validated. See README.md for run and reporting rules.
CoreMark 1.0 : 0.000005 / GCC11.1.0 -DPERFORMANCE_RUN=1  -march=rv64imac -mabi=lp64 -mcmodel=medany -Wno-pointer-to-int-cast -Wno-int-to-pointer-cast -I../driver -O3 -fno-common -funroll-loops -finline-functions -falign-functions=16 -falign-jumps=4 -falign-loops=4 -finline-limit=1000 -fno-if-conversion2 -fselective-scheduling -fno-crossjumping -freorder-blocks-and-partition -DCORE_DEBUG=0  -lgcc -lc -nostartfiles -ffreestanding -Wl,-Bstatic,-T,../common/app.ld,-Map,coremark.map,--print-memory-usage / STACK
4.62 Coremark/MHz
SUCCESS coremark

こうして波形も確認することができる。これはRVCの命令がディスパッチされているところ。 ソースコードでの挙動の予想と違い、RVC命令はAlignerではまだ16ビット命令表現のままだった。

Netflixによるインスタンス負荷改善のための解析事例

LinkedInの記事をめぐっているうちに見つけた、マイクロアーキテクチャに関する面白い事例。 CPUのマイクロアーキテクチャのさらに奥深くまで理解が必要な問題を解決するために、どのようなツールをつかってどのように解決したかの話。

netflixtechblog.com


Netflix内でのワークロード最適化のため、AWSのインスタンスサイズを移行(16 vCPUから48 vCPU)し、CPUがボトルネックとなるワークロードの性能向上を図った。 このインスタンスの移行により、性能をほぼ直線的に増加させることを想定し、スループットがおよそ3倍になると予想した。

しかし、結果としてこの移行で想定する性能は達成できなかった。

https://netflixtechblog.com/seeing-through-hardware-counters-a-journey-to-threefold-performance-increase-2721924a2822 より引用

目標としているCPUの使用率55%に到達してもスループットは平均25%しか増加せず、平均レイテンシは50%以上低下した。 ノード間のばらつきはないものの、CPUとレイテンシのパタンが両方とも大きくぶれるようなグラフになっている。

そこで、ノード毎のCPUとレイテンシの内訳を調べてみると、面白いことが分かってきたらしい。 グラフに示すように、CPU負荷とレイテンシについて、大きく2種類の特性が現れた。 1つ目が、CPUとレイテンシが非常に低く、ばらつきがほとんどない"Lower band"のパタンと、2つ目がCPUとレイテンシのばらつきが非常に大きい"Upper band"が存在するということである。

まずはフレームグラフを確認して、プログラム全体の分布は問題なかった。

そこで、次にインスタンスのパフォーマンス・カウンタを使用してベースラインの情報を集めることから始めた。

"Upper band(Slow node)" と "Lower band(Fast node)"の大きな違いは、CPIが大きく異なっている、そして、MACHINE_CLEARSカウンタにより非常に大きなL1キャッシュのアクティビティがみられる。 このキャッシュライン・アクティビティの大きな違いの主な要因は、False Sharingと呼ばれる本来は必要のないデータをコア館で共有してしまうことによるキャッシュラインの移動によるものである。

この想定のもの、Intel vTuneを使用してマイクロアーキテクチャのプロファイリングを行ったところ、一部の命令で100CPIを超えるコードブロックが見つかった。

このコードによると、複数のスレッドがフィールド①とフィールド⑥の更新を行い、これらが同じキャッシュラインに入った場合、False Sharingが発生することになる。

これが上記の"Upper band"と"Lower band"の比率12.5%に相当する。 つまり、キャッシュラインのサイズは64バイトで、ポインタのサイズは8バイトなので、別々のキャッシュラインに入るのは1/8であり、キャッシュ・ラインを共有してしまう確率は7/8なので、ちょうとこの分布と一致する、ということだ。

解決策としては、上記の2つのフィールドの間にパディングを挿入し、この2つのフィールドが同じキャッシュラインに入らないようにすることだった。 これにより、CPUとレイテンシのグラフが以下の図のように変化し、"Upper band" と "Lower band"の大きな違いはなくなった。

しかしこれでは、まだ目標の性能に到達しないため、再度似たような部分を掘り下げたところ、今度はFalse SharingではなくTrue Sharingの問題が発生していることが分かった。 これは同じ変数が複数のスレッド・コアによって読み書きされることであり、これを回避するためには、JVMのスーパークラスーキャッシュのコア・スレッド間での共有を避けるためのパッチを追加することになる。

これらの修正により、AWSインスタンスの移行により性能は目標とした3.5倍の改善を達成した。


この問題の解析と解決のフローを眺めて、問題を解決するためにはとにかく「測定」に基づく「仮定」によって原因を掴んでいくのが大事だということに改めて気が付かされた。

また、CPUのキャッシュのコヒーレント処理についてはなかなか普通のプログラマには理解が難しいところではあるが、これらのマイクロアーキテクチャの知識がこういったところで性能解析に役立つというのは実に興味深い話だと思った。

オープンソース・アウトオブオーダCPU NaxRiscvを概観する (5. SpinalHDLのソースコード解析)

msyksphinz.hatenablog.com

msyksphinz.hatenablog.com

もうちょっとなんとかSpinal HDLのわけわからん記述が読めるようになりたい: とりあえずRVC Alignerの部分で何とか理解できる部分はないだろうか。

class AlignerPlugin(var decodeCount : Int,
                    var inputAt : Int) extends Plugin with FetchPipelineRequirements with FetchInjector{
/* ... 途中省略 ... */
  val logic = create late new Area {
/* ... 途中省略 ... */

この辺のマスクの実装は良く分からんね。MASK_FRONTがFetchステージからのマスクかしら。 MASK_BACKはどういう意味なんだ?WORD_BRANC_VALID SLICE_COUNT`はフェッチラインの中でRVC命令が何個入るかを示している。

val maskGen = new Area {
      val maskStage = fetch.getStage(inputAt-1)
      import maskStage._
      MASK_FRONT := B((0 until SLICE_COUNT).map(i => B((1 << SLICE_COUNT) - (1 << i), SLICE_COUNT bits)).read(FETCH_PC(sliceRange)))
    }

    MASK_BACK  := B((0 until SLICE_COUNT).map(i => B((2 << i)-1, SLICE_COUNT bits)).read(WORD_BRANCH_SLICE))
    when(!WORD_BRANCH_VALID){ MASK_BACK.setAll() }

以下はフェッチした命令とその情報を格納するバッファを定義しているものと思われる:

    val buffer = new Area {
      val data = Reg(WORD)
      val mask = Reg(MASK_FRONT) init (0)
      val pc   = Reg(PC())
      val fault = Reg(Bool())
      val fault_page = Reg(Bool())
      val branchValid = Reg(WORD_BRANCH_VALID())
      val branchSlice = Reg(WORD_BRANCH_SLICE())
      val branchPcNext = Reg(WORD_BRANCH_PC_NEXT())
      val wordContexts = lastWordContextSpec.map(Reg(_))
      val firstWordContexts = firstWordContextSpec.map(Reg(_))
    }

うーん、decoderについては本当に記述が意味不明だ。

    val decoders = for (i <- 0 until SLICE_COUNT * 2) yield new Area {
      // 命令内のスライスにRVC命令の候補が含まれているか:
      val rvc = RVC.get generate slices.data(i)(1 downto 0) =/= 3

      // RVCが有効な場合:各スライスでどの部分が命令として有効化のビットマスクを作る
      val usage = if(RVC) {
        def mask16 = B(1 << i, SLICE_COUNT*2 bits)
        def mask32 = B(3 << i, SLICE_COUNT*2 bits)
        if(i == SLICE_COUNT*2 - 1) mask16 else rvc ? mask16 | mask32
      } else {
        B(1 << i, SLICE_COUNT*2 bits)
      }

      val notEnoughData = RVC.get match {
        case true =>
          if(i == SLICE_COUNT * 2 - 1)
            !rvc  // 最上位のスライスがRVCではない?
          else if(i == SLICE_COUNT - 1)
            !rvc && (!MASK_FRONT.lsb || !isInputValid) // うーん、これは何のため?
          else
            False
        case false => False
      }


      val pastPrediction = if(i <= SLICE_COUNT) False else WORD_BRANCH_VALID && U(i - SLICE_COUNT) > WORD_BRANCH_SLICE //TODO may use MASK_BACK instead for timings ?
      val usable = !notEnoughData && !pastPrediction
    }