2012-09-07

C言語でのOracle外部プロシージャ

Oracleの外部プロシージャとは、簡単に言うと、PL/SQL以外の言語で書かれたストアドプロシージャのことを指す。 Oracle本家でサポートしているのは以下の3つ。 サードパーティーでは perl が使える。 Oracle® Databaseアドバンスト・アプリケーション開発者ガイドを参照しながらC言語で簡単な外部プロシージャを作ってみる。
まず、system ユーザーでログインして試験用ユーザーを作成
~$ sqlplus

SQL*Plus: Release 10.2.0.1.0 - Production on Fri Sep 7 13:22:00 2012

Copyright (c) 1982, 2005, Oracle.  All rights reserved.

Enter user-name: system/manager

Connected to:
Oracle Database 10g Express Edition Release 10.2.0.1.0 - Production

SQL> create user extproc_test identified by extproc_test                    
  2  default tablespace users temporary tablespace temp;

User created.

SQL> grant connect, resource, create library to extproc_test;

Grant succeeded.

次に試験用ユーザーでログインして、共有ライブラリの場所を指定。
SQL> connect extproc_test/extproc_test 
Connected.
SQL> create library extproc_test as '${ORACLE_HOME}/lib/extproc_test.so';
  2  /

Library created.

$ORACLE_HOME/lib, $ORACLE_HOME/bin 以外の場所に共有ライブラリを置くときは 環境変数EXTPROC_DLLSを設定すること。
次にSQL関数を作成。
SQL> create function add_func(x pls_integer, y pls_integer)                       
  2  return pls_integer
  3  as language C library extproc_test name "add_func";
  4  /

Function created.

次にC言語の関数を作成。
int add_func(int x, int y)
{
  return x + y;
}
pls_integer はデフォルトでは C 言語の int にマッピングされる。
コンパイルして $ORACLE_HOME/lib にコピー。
$ gcc -shared -o extproc_test.so extproc_test.c
$ cp extproc_test.so $ORACLE_HOME/lib
実行してみる。
SQL> select add_func(1, 2) from dual;

ADD_FUNC(1,2)
-------------
            3
期待通りに動いた。
試しにNULLを渡してみる。
SQL> select add_func(1, NULL) from dual;
select add_func(1, NULL) from dual
                              *
ERROR at line 1:
ORA-01405: fetched column value is NULL
C言語では NULL かどうかの判断ができないから当然か。。。
C言語側にNULLかどうかのフラグを渡せるよう関数定義を修正する。
SQL> create or replace function add_func(x pls_integer, y pls_integer)
  2  return pls_integer
  3  as language c library extproc_test name "add_func"
  4  parameters(x, x indicator, y, y indicator, return indicator);
  5  /

Function created.

parameters の指定に従い、C言語の関数定義も変更する。
#include 

int add_func(int x, short x_ind, int y, short y_ind, short *rind)
{
  if (x_ind || y_ind) { /* x または y が NULL のとき */
    *rind = OCI_IND_NULL; /* 戻り値は NULL */
    return 0;
  }
  *rind = 0; /* 戻り値は非NULL */
  return x + y; /* 戻り値 */
}
再度コンパイルしてコピー。
$ gcc -shared -o extproc_test.so extproc_test.c -I$ORACLE_HOME/rdbms/public
$ cp extproc_test.so $ORACLE_HOME/lib
実行してみる。
SQL> select add_func(1, NULL) from dual;
select add_func(1, NULL) from dual
                              *
ERROR at line 1:
ORA-28576: lost RPC connection to external procedure agent
使用中の共有ライブラリを上書きしたせいか、それともメモリ上には古い共有ライブラリが 載っているのにOracle側の関数定義を変えたせいか。 まあ、どちらにしても、これでextprocプロセスが再起動してくれれば次はうまく行くはず。
SQL> select add_func(1, NULL) from dual;

ADD_FUNC(1,NULL)
----------------

今度は動いた。
念のため非NULL値の試験も行う。
SQL> select add_func(1, 2) from dual;

ADD_FUNC(1,2)
-------------
            3
動いた。

以上。

2012-07-21

SSE4.2 の crc32c 命令の呼び出し

SSE4.2 に追加された crc32c 用の機械語命令を使うのに必要そうな機能をメモ書き。 ちなみに、SSE4.2 対応の CPU を持ってないので、未検証の項目多い。

SSE4.2対応のCPU

  • Intel系はNehalemマイクロアーキテクチャ以降
  • AMD系はBulldozerマイクロアーキテクチャ以降
Wikipedia によるとNehalemマイクロアーキテクチャは「 主に2008年〜2011年ごろに発売された。」と書いてあるので、最近のデスクトップ用のCPUならば大丈夫?

SSE4.2対応のCPUかどうかのチェック

(2012-11-03 大幅書き換え)

当然だが、SSE4.2 に対応してないCPUでは crc32c 機械語命令は使えないので、事前に SSE4.2 に対応しているかどうかをチェックする必要がある。
以下の関数 sse4_2_is_supported() を呼び出して、戻り値が 1 ならばSSE4.2対応、0の場合は非対応。
#define CPUID_ECX_BIT_SSE4_2 (1u << 20)

/* GNU C Compiler */
#if defined __GNUC__
#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3)
#include <cpuid.h>
/* gcc version >= 4.3 */
int sse4_2_is_supported(void)
{
  unsigned int eax, ebx, ecx, edx;
  __cpuid(1, eax, ebx, ecx, edx);
  return (ecx & CPUID_ECX_BIT_SSE4_2) ? 1 : 0;
}
#else
/* gcc version < 4.3 */
int sse4_2_is_supported(void)
{
  unsigned int ecx;
#if defined(__i386__) && defined(__PIC__)
  __asm("movl $1, %%eax;"
        "pushl %%ebx;"
        "cpuid;"
        "popl %%ebx;"
        : "=c" (ecx)
        :
        : "eax", "edx");
#else
  __asm("movl $1, %%eax;"
        "cpuid;"
        : "=c" (ecx)
        :
        : "eax", "ebx", "edx");
#endif
  return (ecx & CPUID_ECX_BIT_SSE4_2) ? 1 : 0;
}
#endif

/* Microsoft Visual C++ */
#elif defined _MSC_VER
#if _MSC_VER >= 1400
#include <intrin.h>
/* msvc version >= 2005 */
int sse4_2_is_supported(void)
{
  int cpuinfo[4];
  __cpuid(cpuinfo, 1);
  return (cpuinfo[2] & CPUID_ECX_BIT_SSE4_2) ? 1 : 0;
}
#else
/* msvc version < 2005 */
int sse4_2_is_supported(void)
{
  unsigned int c;
  __asm {
    mov eax, 1
    cpuid
    mov c, ecx
  }
  return (c & CPUID_ECX_BIT_SSE4_2) ? 1 : 0;
}
#endif

#else
#error unsupported compiler
#endif

crc32c命令の呼び出し

(2012-11-03 大幅書き換え)

gcc 4.3以上、icc 12.0 以上、Visual C++ 2008 以上ならば、 以下のIntrinsicsが使える。
  • unsigned int _mm_crc32_u8 (unsigned int crc, unsigned char v);
  • unsigned int _mm_crc32_u16 (unsigned int crc, unsigned short v);
  • unsigned int _mm_crc32_u32 (unsigned int crc, unsigned int v);
  • unsigned long long _mm_crc32_u64 (unsigned long long crc, unsigned long long v);(Linux)
  • unsigned __int64 _mm_crc32_u64 (unsigned __int64 crc, unsigned __int64 v);(Windows)
SSE4.2 対応のCPUを持ってないので、実機での試験はしてないが、以下のコードでOKなのではないかと思う。
#include <nmmintrin.h>

unsigned int crc32c_sse4_2(unsigned int crc32c, const char *buffer, size_t length)
{
  size_t quotient;

#if 0 /* 未検証だがアライメントを揃える必要はなさそう。 */
  /* buffer の先頭部分のアライメントに沿ってない部分の処理 */
  if ((((size_t)buffer) & 1) && length >= 1) {
    crc32c = _mm_crc32_u8(crc32c, *(unsigned char*)buffer);
    buffer += 1;
    length -= 1;
  }
  if ((((size_t)buffer) & 2) && length >= 2) {
      crc32c = _mm_crc32_u16(crc32c, *(unsigned short*)buffer);
      buffer += 2;
      length -= 2;
    }
#if defined(__x86_64) || defined(_M_X64)
  if ((((size_t)buffer) & 4) && length >= 4) {
    crc32c = _mm_crc32_u32(crc32c, *(unsigned int*)buffer);
    buffer += 4;
    length -= 4;
  }
#endif
#endif

  /* アライメントに沿っている部分の処理 */
  quotient = length / sizeof(size_t);
  while (quotient--) {
#if defined(__x86_64) || defined(_M_X64)
    crc32c = _mm_crc32_u64(crc32c, *(size_t*)buffer);
#else
    crc32c = _mm_crc32_u32(crc32c, *(size_t*)buffer);
#endif
    buffer += sizeof(size_t);
  }

  /* buffer の後ろの部分のアライメントに沿ってない部分の処理 */
#if defined(__x86_64) || defined(_M_X64)
  if (length & 4) {
    crc32c = _mm_crc32_u32(crc32c, *(unsigned int*)buffer);
    buffer += 4;
  }
#endif
  if (length & 2) {
    crc32c = _mm_crc32_u16(crc32c, *(unsigned short*)buffer);
    buffer += 2;
  }
  if (length & 1) {
    crc32c = _mm_crc32_u8(crc32c, *(unsigned char*)buffer);
  }
  return crc32c;
}
アライメントを気にした処理をしているが、アライメントを揃える必要が本当にあるかは不明...。Intel の Optimization Reference マニュアルの CRC32 のサンプルはアライメントを気にしてないので、アライメントを揃える必要はなさそう。
ちなみに gcc の場合コンパイルオプションに -msse4.2 を付ける必要がある。

SSE4.2の有無により呼び出す関数を分ける方法

例えば SSE4.2 があるときはcrc32c_sse4_2()を呼び出して、SSE4.2 がないときは C で実装した関数crc32c_implemented_by_c()を呼び出したい場合、以下のように関数ポインタを使用することが多い。
unsigned int (*crc32c)(unsigned int crc32, const char *p, size_t len);

int main()
{
    ...初期化時に関数ポインタ crc32c に実際に呼び出す関数を設定 ...
    if (sse4_2_is_supported()) {
        crc32c = crc32c_sse4_2;
    } else {
        crc32c = crc32c_implemented_by_c;
    }

    ...
    crc = crc32c(crc, buf, buflen);  // crc32c関数の呼び出し
}

最近の gcc ならば、 ifunc というアトリビュートがあり、これを使うとプログラムのロード時にどの関数を呼び 出すかをチェックできる。
// この関数はプログラムのロード時、constructor の呼び出し前に実行される。
unsigned int (*resolve_crc32c(void))(unsigned int crc32, const char *p, size_t len)
{
    if (sse4_2_is_supported()) {
        return crc32c_sse4_2;
    } else {
        return crc32c_implemented_by_c;
    }
}

unsigned int crc32c(unsigned int crc32, const char *p, size_t len)  __attribute__ ((ifunc ("resolve_crc32c")));

int main()
{
    ...
    crc = crc32c(crc, buf, buflen);  // crc32c関数の呼び出し
}
で、出力されたアセンブリを見てみると、、、crc32c のような重い関数の場合、関数内部の負荷と比べると、関数呼び出し部分の微細な最適化はあまり意味がないような気がしてきた。