はぐれエンジニアつれづれ

デジタルLSI設計(SystemC, SystemVerilog, etc.)の備忘録。はてなダイアリから移行中。

CMake + Google Testの中規模プロジェクト作成

はじめに

ディレクトリ構成

Directory Note
include/${PROJECT_NAME} 公開ヘッダファイル
src ソースコード, 非公開ヘッダファイル
test テスト記述

GoogleTestのインストール

  • release-1.10.0を~/local/gtest-1.10.0にインストール
git clone https://github.com/google/googletest.git -b release-1.10.0 --depth=1
cd googletest
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=~/local/gtest-1.10.0 ..
make -j 8 install

プロジェクトのビルドと実行

git clone https://github.com/taksei/googletest_sample.git
cd googletest_sample
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_TEST=ON
make -j 8
# Run all tests
make test
# Run specified test
./test/test_sample

Linuxコマンド

つれづれに追加していきます。

指定した文字列をファイル名に含むファイルを検索する。

find -name "文字列" .

指定した文字列を内容に含むファイルを検索する。

grep "文字列" -rl .

テンポラリファイルを作成せずに、2つの生成した標準出力を比較する。

diff <(command1 )  <(command2)

重複する行を取り除く。

cat ${file} | sort | uniq

重複する行の数を出力する(CSV形式)。

cat ${file} | sort | uniq -c | sed "s/^\s\+//" | sed "s/\s\+/,/

特定文字で区切ったフィールドを抜き出す。

cat ${file} | cut -d ',' -f 3,4,8

GitリポジトリのHEADが示すコミットのSHAを出力する

git rev-parse HEAD

コード生成のためのヒアドキュメント利用

CSVで指定されたパラメータセットに応じてコードを自動生成するスクリプト
パラメータの取得はPython標準CSVパーサを利用。コード生成はヒアドキュメントに変数を埋め込むことで実現している。

#! /usr/bin/env python

import sys
import csv

#-----------------------------------------
# Load config.csv
#-----------------------------------------
args = sys.argv
with open(args[1]) as f:
  for row in csv.reader(f):
    if not row[0].startswith('#'):
      config = row

__P0 = int(config[0].replace(' ', ''))
__P1 = int(config[1].replace(' ', ''))
__P2 = int(config[2].replace(' ', ''))

#-----------------------------------------
# Generate param.h
#-----------------------------------------

str='''
#pragma once
#define PARAM0 {__p0}
#define PARAM1 {__p1}
#define PARAM2 {__p2}

const float A[PARAM0 * PARAM1] = {{
#include "./data/{__p0}_{__p1}.csv"
}}

const float B[PARAM1 * PARAM2] = {{
#include "./data/{__p1}_{__p2}.csv"
}}

float Y[PARAM0 * PARAM2];
'''.format(__p0=str(__P0), __p1=str(__P1), __p2=str(__P2)).strip()

print(str)

パラメータ設定ファイル例

# p0, p1, p2
5, 3, 4

Pythonのヒアドキュメントは'''と'''で囲んで作成する。
上記の記述例では、開始の'''の直後と、終了の'''の直前に改行が挿入されるため、stripにて前後の改行を取り除く。
ヒアドキュメント内の変数展開はformatメソッドを使用する。ヒアドキュメント内で{変数名}として展開する変数を記述する。なお、本来の{および}は変数と区別するため、{{、}}と書き換える。

メルセンヌツイスターによるランダムテストデータ作成

ランダムなテストデータを生成する関数randomizeArrayを作成した。
具体的には、指定した区間([min, max))の一様分布にて配列をランダム化する。

疑似乱数であるstd::mt19937_64を使っているため同一環境での再現性あり。
ただし、一様分布生成std::uniform_real_distributionの実装が環境依存であるため、異なる環境での再現性は保証されない。
コンパイルにはC++11オプションの指定が必要。

#include <iostream>
#include <fstream>
#include <random>

template<typename T>
void randomizeArray
(
  T *X,
  const int n,
  T max = 1.0,
  T min = -1.0,
  int seed = 0
)
{
  static std::mt19937_64 mt(seed);
  std::uniform_real_distribution<> dist(min, max);
  for (int i=0; i<n; ++i)
  {
    X[i] = static_cast<T>(dist(mt));
  }
}

template<typename T>
void printMat
(
  T *X,
  const int m,
  const int n,
  std::ostream& os = std::cout
)
{
  for (int i = 0; i < m-1; ++i)
  {
    for (int j = 0; j < n; ++j)
    {
      os << X[i*n+j] << ",";
    }
    os << std::endl;
  }
  for (int j = 0; j < n-1; ++j)
  {
    os << X[(m-1)*n + j] << ",";
  }
  os << X[m*n-1] << std::endl;
}

int main(void)
{
  int M = 5;
  int K = 3;
  int N = 4;

  float *A = new float[M*K];
  float *B = new float[K*N];

  randomizeArray<float>(A, M*K, 100, -100);
  randomizeArray<float>(B, K*N, 100, -100);

  std::cout << "# float A(" << M << "," << K << ")" << std::endl;
  printMat<float>(A, M, K);
  std::cout << "# float B(" << K << "," << N << ")" << std::endl;
  printMat<float>(B, K, N);

  int *A_ = new int[M*K];
  int *B_ = new int[K*N];

  std::cout << "# int A_(" << M << "," << K << ")" << std::endl;
  randomizeArray<int>(A_, M*K, 100, -100);
  std::cout << "# int B_(" << K << "," << N << ")" << std::endl;
  randomizeArray<int>(B_, K*N, 100, -100);

  printMat<int>(A_, M, K);
  printMat<int>(B_, K, N);

  return 0;
}

出力は以下の通り。

# float A(5,3)
-68.0413,98.429,-92.0862,
19.4989,8.45699,-88.568,
26.3057,-15.2859,68.4521,
81.2603,-15.3704,31.1052,
86.0862,-31.1229,-49.077
# float B(3,4)
-11.0658,58.0379,39.0841,11.5361,
-42.5139,-70.8368,15.8528,12.9099,
-70.7164,-69.9669,-96.0132,10.8815
# int A_(5,3)
-68,98,-92,
19,8,-88,
26,-15,68,
81,-15,31,
86,-31,-49
# int B_(3,4)
-11,58,39,11,
-42,-70,15,12,
-70,-69,-96,10

std::mt19937_64にstatic修飾子を付加することで、randomizeArrayのコール毎に異なる値が出力される。配列A、Bで異なる乱数列が生成されている。

ただし、テンプレート引数が異なる場合、内部で同じ乱数列が生成されるため、同じ数値を異なる型でキャストされた値が出力される。配列AとA、BとBはそれぞれ同じ乱数列をfloat、intでキャストした値が生成されている。

float, doubleの16進数内部表現から値を復元する

16進数内部表現で与えられたfloatやdoubleの変数値を入力として、floatやdoubleに復元するコードを考えた。

入力となる16進数内部表現には、先頭に”0x”または"0X"が付加されていてもよい。

x86_64にて動作確認。処理系依存のコードになっているため、使用時には注意が必要。

#include <iostream>

float hex2float(const std::string& hex)
{
  long hex_l = std::stol(hex, nullptr, 16);
  float *hex_pf = reinterpret_cast<float*>(&hex_l);
  return *hex_pf;
}

double hex2double(const std::string& hex)
{
  long long hex_ll = std::stoll(hex, nullptr, 16);
  double *hex_pd = reinterpret_cast<double*>(&hex_ll);
  return *hex_pd;
}

int main(void)
{
  std::string fstr1 = "40490E56";
  float f1 = hex2float(fstr1);
  std::cout << "float, " << fstr1 << " = " << f1 << std::endl; 

  std::string fstr2 = "0x40490E56";
  float f2 = hex2float(fstr2);
  std::cout << "float, " << fstr2 << " = " << f2 << std::endl; 

  std::string dstr1 = "400921CAC083126F";
  double d1 = hex2double(dstr1);
  std::cout << "double, " << dstr1 << " = " << d1 << std::endl; 

  std::string dstr2 = "0x400921CAC083126F";
  double d2 = hex2double(dstr2);
  std::cout << "double, " << dstr2 << " = " << d2 << std::endl; 

  return 0;
}

実行結果は以下の通り

float, 40490E56 = 3.1415
float, 0x40490E56 = 3.1415
double, 400921CAC083126F = 3.1415
double, 0x400921CAC083126F = 3.1415

ユーザ定義クラスのためのメソッド定義

SystemCでは、ユーザ定義型(クラス)をチャネルのテンプレート引数として使用できる。
ユーザ定義型には、①比較(==)、②標準出力(<<)、③sc_traceの3つの関数が必要とされる。
特に、①はコンパイルエラー回避のため必須である。
②、③は無くてもよいが、デバッグ容易化のために追加が望ましい。

以下では、位置を示す2変数を保持するユーザ定義型pointを例に、①、②を実装した。

まずは、ユーザ定義型のヘッダファイル。
oprerator<<はメンバ関数ではなく、friend関数とする。

#pragma once
#include <iostream>

class point
{
  private:
    int x;
    int y;

  public:
    point(int x, int y); 
    ~point(void);
    bool operator==(const point& obj) const;
    friend std::ostream& operator<<(std::ostream& str, const point& obj);
};

次に、ユーザ定義型の実装ファイル。

#include "userdef.hpp"

point::point(int x, int y)
  : x(x), y(y)
{
};

point::~point(void)
{
};

bool point::operator==(const point& obj) const
{
  return ((this->x == obj.x) && (this->y == obj.y));
};

std::ostream& operator<<(std::ostream& str, const point& obj) 
{
  str << obj.x << "," << obj.y;
  return str;
};

最後に、メイン関数からの呼び出し。

#include <iostream>
#include "userdef.hpp"

int main(void)
{
  point p1( 10, 20 );
  point p2( 11, 20 );
  std::cout << p1 << std::endl;
  std::cout << p2 << std::endl;
  std::cout << (p1 == p2) << std::endl;
  return 0;
};

実行結果は以下の通り。

10,20
11,20
0

次回は、久しぶりにSystemC環境を構築してみる。