任意精度型変数を使用して無限ループに陥った失敗談【原因と対策】

Xilinx SoC
スポンサーリンク

本記事の概要

前回、任意精度型ライブラリの使い方に関する解説記事をまとめました。

任意精度型ライブラリは高位合成を行う上で、必要最小限のリソースで変数のビットサイズを指定できる有用なデータ型を提供してくれます。

以前、上記の記事で高位合成のためにテストベンチを作ってCシミュレーションを回したところ、無限ループに陥り処理が全然終わらないというトラブルがありました。

なぜだろうと悩んでいたところ、以下のような初歩的なミス…。

今回は備忘録も兼ねて、失敗談を交えつつ、どうすれば失敗を避けられたのかという対策をまとめておこうと思います。

ひがし

コラムとして楽しんで読んでいただければと思います!

問題のプログラム

まずは、問題のプログラムを見てみましょう。

int main()
{
    ap_uint<2>     aloop, bloop;
    ap_uint<4>     c_hard;

    for (aloop = 0; aloop <= 3; aloop++) {
        for (bloop = 0; bloop <= 3; bloop++) {
            /* 略 */
            swtoled(&aloop, &bloop, &c_hard);

            /* 略 */
        }
    }
}

プログラムでやりたかったことは、以下の通りです。

プログラムの内容

掛け算IPの機能検証のため、

  1. 引数aloopとbloopを0から3まで1ずつ刻む
  2. 各組み合わせに対してaloop × bloopの計算結果を出力。機能検証。
  3. 3まで終わればループを抜ける

という処理を、Forループを使って行う

しかし、このループは終わりませんでした。なぜでしょうか?

原因

原因は変数aloopとbloopが取りうる範囲にあります。
この変数のデータ型は任意精度整数型ap_uint<2>で、2ビットの符号なし整数を表現しています。

2ビットの符号なし整数の取りうる範囲は0~3です。

そのため、次のForループは条件を常に満足し続けるため終わりません。

    ap_uint<2>     aloop;

    for (aloop = 0; aloop <= 3; aloop++) {
        /* 略 */
    }

図を使って見ていきましょう。
aloop = 3に1を足した値は4ではなく0に戻るので、ループは図のように0-3の範囲をぐるぐると回り続けます。
「aloop <= 3」の条件は常に満たされ、ループが終わらないわけです。
まさに無限ループに陥るのです。

考えてみたら当たり前ですが、ap_uintの取りうる範囲を意識しないと盲点になります。
失敗の原因はint型と同じ感覚で、変数の取りうる範囲を意識せず任意精度型ap_uintを使用してしまったためです。
int型と同じような感覚で適当に任意精度型を使うと、変数が取りうる最大値が小さく想定外のバグに陥る危険性があるということに気づきました。

対策

対策例は以下の2点があると思います。
あくまでも私見ですので、他の有効な対策もあるかもしれません。

対策例
  1. テストベンチでは任意精度型を避ける
  2. 変数の取りうる範囲を常に意識する

1. テストベンチでは任意精度型を避ける

前回の記事「Vitis HLSの任意精度型ライブラリの使い方について解説」でもまとめましたが、任意精度型を使うメリットは「FPGAで高位合成したときに必要最小限のリソースに抑えることができる点」にあります。

高位合成対象の関数では、任意精度型を使うメリットを十分活かせます。

一方で、テストベンチでは、int型やfloat型を使っても高位合成後のロジックのリソースを圧迫することはないので、任意精度型をあえて使わなくても良いように思います。
そこで、筆者はテストベンチではできるだけ任意精度型での変数の宣言を避けるようにしています。
ただし、高位合成対象の関数に代入するときには、データ型の変換が必要です。

例えば、次のコードのようにaloopとbloopはint型で宣言をしておき、ap_uint型の変数aとbに型変換を行ったあと、高位合成対象の関数swtoled()に代入するとよいと思います。

int main()
{
    int			       aloop, bloop;
    ap_uint<2>     a, b;
    ap_uint<4>     c_hard;

    for (aloop = 0; aloop <= 3; aloop++) {
        for (bloop = 0; bloop <= 3; bloop++) {
            /* 略 */
            a = (ap_uint<2>)aloop;
            b = (ap_uint<2>)bloop;
            swtoled(&aloop, &bloop, &c_hard);

            /* 略 */
        }
    }
}

2. 変数の取りうる範囲を常に意識する

任意精度型では変数の取りうる範囲を意識することも有効だと思います。
int型では4バイト(32ビット)で数を表し、「-2,147,483,648~2,147,483,647」の範囲を取ります。
日常の感覚から言うと、かなり大きな数なので、もしかしたらint型が取りうる範囲をほとんど意識することはないかもしれません。

しかし、char型の変数ではどうでしょうか。
char型は1バイト(8ビット)で数を表し、「-128~127」の範囲を取ります。
ループを組むには、少し変数の取りうる範囲は少なめかもしれません。
char型でループを組むと、すぐに上限値に達してしまいそうですね。

ひがし

余談ですが、かつてのファミコンのゲームなどはROM容量が少なく、変数のビット数をあまり大きくはできませんでした。変数の取りうる範囲の小ささを利用し、あえて上限値を超えてバグを引き起こしたり、乱数を固定したりするなどの裏技があったことを思い出します。

参考:FC版ドラゴンクエストIVに存在するバグ技「8逃げ」

任意精度型はchar型の8ビットよりもさらに小さいビット数を指定できるため、取りうる範囲をより小さくすることが可能です。例えば

ap_uint<4>であれば「0~24-1」(0~15)の範囲
ap_int<4>であれば「-23~23-1」(-7~8)の範囲

です。そのため、char型以上に変数の取りうる範囲に気を配る必要があると思います。
int型ではあまり意識をすることはありませんが、ビット数の小さい変数では変数の取りうる範囲を常に意識することが、ロジック全体のリソースを最適化し、かつバグを減らす上で重要になります。

まとめ

以上をまとめると、この通りです。

本記事のまとめ
  • [問題点] 任意精度型の変数を使ってループを組んだところ、無限ループに陥った
  • [原因] 変数のビット数が少なく取りうる範囲が小さかったため、ループを抜ける条件を満たさなかった
  • [対策例]
    1. テストベンチでは任意精度型を避ける
    2. 変数の取りうる範囲を常に意識する
ひがし

あまりにも初歩的なミスですが、教訓にしたいと思います。
ここまで読んでいただき、ありがとうございました。

コメント