前置き:トピック
- Julia の
reinterpret()
関数の紹介 -
reinterpret()
を利用した構造体の初期化 -
reinterpret()
の注意点 【2016/07/14 編集】v0.5.0-dev 含む最新の情報に更新- 【2018/08/18 編集】正式版 v1.0.0 の情報に合わせて大幅加筆・修正1
Julia の reinterpret()
関数
Julia には reinterpret()
という関数が用意されています。
APIドキュメント2によれば、以下の機能:
Change the type-interpretation of a block of memory.
つまり、メモリ上に並んだバイト列を別の型に再解釈する、ということ。
主な使い方は、2通り。
- 同じバイトサイズの型どうしを互いに変換(再解釈)する。
- 配列を別の型の配列に変換(再解釈)する。
以下、簡単な例を挙げておきます。3。
1. 同じバイトサイズの型どうしを互いに変換(再解釈)する。
まずは見ていただいた方が早いと思うので↓。
i = Int64(1) # 型が Int64 であることを明示するためにわざとこんな書き方してみる
reinterpret(Float64, i)
# => 5.0e-324
rmax = floatmax(Float64)
# => 1.7976931348623157e308(倍精度実数で表現できる最大の有限値)
reinterpret(UInt64, rmax)
# => 0x7fefffffffffffff(↑のビット表現を16進数16桁で表現)
reinterpret(Float32, 'あ')
# => -4.777995f21(Char は Julia では 4バイト(32ビット))
reinterpret(Int8, true)
# => 1(Bool は 1バイト(ただし最下位ビットのみ使用))
簡単に説明しなおすと、「第2引数で指定した値(数値)を、バイトサイズが同じ別の型の値として再解釈」。
もう少し詳しく言うと、変換(再解釈)できる元の型と先の型は、バイトサイズが同じ Bits Type4 どうし、です。
元と先の型のサイズ(sizeof()
関数でバイトサイズが取得できます)が異なる場合(例:reinterpret(Float32, convert(Int64, 1))
)、"ERROR: bitcast: argument size does not match size of target type" というエラーとなります5。
またそもそも Bits Type でない場合(例:reinterpret(Int32, "あ")
)、"ERROR: bitcast: expected primitive type value for second argument" というエラーになります。
2. 配列を別の型の配列に変換(再解釈)する。
こちらもまずは↓例から:
# 例1:同じサイズの型どうしの(一括)変換
int64s = Int64[1, 2, 3, 4, 5]
# typeof(int64s) == Array{Int64, 1}
fs = reinterpret(Float64, int64s)
# => Float64[5.0e-324, 1.0e-323, 1.5e-323, 2.0e-323, 2.5e-323]
# typeof(fs) # => Base.ReinterpretArray{Float64,1,Int64,Array{Int64,1}}
# 例2:大きいサイズ→小さいサイズの型への変換
# ↑のint64sをそのまま利用
reinterpret(Int32, int64s)
# => Int32[1, 0, 2, 0, 3, 0, 4, 0, 5, 0]
# 例3:小さいサイズ→大きいサイズの型への変換
int32s = Int32[1, 2, 3, 4]
reinterpret(Int64, int32s)
# => Int64[8589934593, 17179869187]
reinterpret(UInt64, int32s) # Julia v0.3.x の場合は `UInt64` → `Uint64`
# => UInt64[0x0000000200000001, 0x0000000400000003]
# 例3-1:小さいサイズ→大きいサイズの型への変換(サイズが合わない場合)
reinterpret(UInt64, Int32[1, 2, 3, 4, 5])
# @> ERROR: ArgumentError: cannot reinterpret an `Int32` array to `UInt64` whose first dimension has size `5`.
# The resulting array would have non-integral first dimension.
少し解説:
- 例1:ある型の配列から同じサイズの別の型の配列への変換(再解釈)。パターン1の変換(再解釈)を一括で行っているようなイメージ。これもそんなに難しくないと思います。
- 1点注意点として、変換後の値の型は
Array{Float64,1}
ではなく、Base.ReinterpretArray{Float64,1,Int64,Array{Int64,1}}
という特殊な型になります6。
ただしこれはBase.ReinterpretArray{Float64,1,Int64,Array{Int64,1}} <: AbstractArray{Float64,1}
となるので、普通の配列として扱うことは出来る、ということ。
なのでこの記事ではこれ以降も「変換後もまるで普通の配列であるかのように」扱います。
- 1点注意点として、変換後の値の型は
- 例2:大きいサイズ→小さいサイズの型への変換(再解釈)。この場合、
Int64
が擁する 8Bytes を、下位バイトから7 4Bytes ずつに分けてInt32
として再解釈して、再び配列として再構築(再解釈後の型はBase.ReinterpretArray{Int32,1,Int64,Array{Int64,1}}
(<:AbstractArray{Int32,1}
))、という仕組みになっています。
つまりInt64
の1
(==0x0000000000000001
)を半分に割って下位から1, 0
(==0x00000001, 0x00000000
)、というわけ。 - 例3:↑の逆。
Int32
の1, 2
(==0x00000001, 0x00000002
)を先の値が下位になるように繋げて8589934593
(==0x0000000200000001
)、ということ。- 例3-1:配列のサイズが合わずはみ出す要素がある場合、このような「再解釈できません」というエラーになります8。
さらに。
配列の変換の場合、要素の型は Bits Type である必要は無く、"Plain Data" Type(=isbits()
9 が true
を返す型10)ならOK。
ということで追加例です。
# 例4:複素数型の実数部・虚数部への分解
c = 1.0 + 2.0im
# typeof(c) # => Complex{Float64}
# isbits(c) # => true
# sizeof(c) # => 16
re_im = reinterpret(Float64, [c])
# => Float64[1.0, 2.0]
# 参考:reim(c) # => (1.0, 2.0)
# 例5:↑の逆、しかもInt64(UInt64)からInt32の複素数型の構築
reinterpret(Complex{Int32}, [0x0000000200000001])[1]
# => Complex{Int32}[1+2im]
# 例6:tupleの配列から数値型への変換
t = (Int32(1),Int32(2))
# isbits(t) # => true(つまり Tuple も "Plain Data" Type)
# sizeof(t) # => 8
reinterpret(UInt64, [t])
# => UInt64[0x0000000200000001]
# 例7:数値の配列からtuple(の配列)への変換
reinterpret(NTuple{2,Int32},[0x0000000200000001])
# => Tuple{Int32, Int32}[(1, 2)]
これらの第2引数、[
/]
を外すと "ERROR: bitcast: expected primitive type value for second argument" というエラーになります。第2引数が配列の場合にだけ許されている、ということです。なお戻り値も配列なので、1つの値を他の型の値に変換(再解釈)したい場合には、例5に示したように reinterpret(〜)[1]
のようにして戻り値から値を取り出す必要があります。
immutable な Composite Type
Julia では、独自に定義した型も以下の条件を満たせば、"Plain Data" Type になります(=isbitstype()
が true
を返すようになります)1112:
-
mutable struct
ではなくstruct
で定義している(=変更不可能な型である)。 - 要素が全て "Plain Data" Type である。
ということで、これも reinterpret()
に渡すことができます(配列利用)。
具体例を挙げてみます。
# あるデータ構造
struct CQDate
j::Int32
y::Int16
m::Bool
leap::Bool
end
sizeof(CQDate) # => 8
isbitstype(CQDate) # => true
# ↑のあるデータ
qt = CQDate(604491, 1655, true, false)
# ↑をInt64にreinterpret
qt_i = reinterpret(Int64, [qt])[1]
# => 288583148190027
# 確認:
604491 | (1655<<32) | (1<<48) | (0<<56)
# => 288583148190027
# 逆変換も確認
reinterpret(CQDate, [288583148190027])[1]
# => CQDate(604491,1655,true,false)
これを利用して、大量のデータを一括で Julia のデータ構造に変換できます13:
# あるデータ構造
struct CQDate
j::Int32
y::Int16
m::Bool
leap::Bool
end
# ↑の元データ(抜粋)
QT_DATA = Int64[281474976710656,30,59,89,118,72057594037928084,177,207]
# ↓データ変換
qt = reinterpret(CQDate, QT_DATA)
# => 8-element reinterpret(CQDate, ::Array{Int64,1}):
# CQDate(0,0,true,false)
# CQDate(30,0,false,false)
# CQDate(59,0,false,false)
# CQDate(89,0,false,false)
# CQDate(118,0,false,false)
# CQDate(148,0,false,true)
# CQDate(177,0,false,false)
# CQDate(207,0,false,false)
この方法の利点は、(元データのメモリブロックをそのまま利用しコピーしないので)メモリ消費が抑えられ、かつパフォーマンスも良いこと。なので、ソースコード内に書けるレベルの量(数万件程度まで)くらいなら(あとエンディアンの問題713が解決できていれば)かなり有効だと思います。
注意点
(エンディアンの問題の他に)この方法は、一つ問題があります。
reinterpret()
はあくまで「メモリブロックの再解釈」であり、「値をコピーして変換」しているわけではありません。よってそのメモリ内容が変更されると、変換後(再解釈後)の値も変わってしまいます(同じメモリを参照しているので)。
具体例↓:
# あるデータ構造
struct CQDate
j::Int32
y::Int16
m::Bool
leap::Bool
end
# ↑の元データ(抜粋)
QT_DATA = Int64[281474976710656,30,59,89,118,72057594037928084,177,207]
# ↓データ変換
qt = reinterpret(CQDate, QT_DATA)
# 確認
qt[8]
# => CQDate(207,0,false,false)
qt[8].leap
# => false
# 汚染
QT_DATA[8] |= 1<<56
# 再確認
qt[8]
# => CQDate(207,0,false,true)
qt[8].leap
# => true
つまり、最後の要素(leap
)が false
から true
に変わってしまっています。immutable にも関わらず要素の値が変更できてしまっているのです!
これは「知っててもやらないようにする」という暗黙の取り決めが必要ですね(^-^;
参考
-
この記事は、「過去に Qiita で書いた Julia 記事のコードを最新 v1.0 で動くものにリライトしよう」という超個人プロジェクトの第2弾になります。v0.5→v1.0で特に注目すべき変更点は、本文に加筆または脚注追加していきます。また編集履歴を見ていただければ旧バージョン用の記事内容もご覧いただけます。 ↩
-
以下のコード及び説明は、Julia v1.0.0 にターゲットを絞っています(動作確認しています)。v0.7.0 でも動作しますが、0.6.x 以前では動作しないか、挙動が一部異なります(説明が当てはまらない箇所があります)。 ↩
-
例外として、v0.4.x までは、(16/32/64/128Bitsの)整数値から Bool への再解釈(例:
reinterpret(Bool, 1) # => true
)が認められていました。結果はisodd(n)
と一致します。この挙動は 0.5.0-dev 以降で改められた模様です。Int8
/UInt8
以外の数値を再解釈しようとすると、ERROR: bitcast: argument size does not match size of target type
(ver.1.0.0 の場合) という非常に妥当なエラーになります。 ↩ -
以前(v0.6.x まで)は、変換後は普通の配列(例:
Array{Float64,1}
)となっていました。v0.7.0/v1.0.0 から仕様変更となった模様です。 ↩ -
これは Julia の内部メモリのバイトオーダーがリトルエンディアンだから起きていることなのですが、Julia がというより実行しているマシンのアーキテクチャがリトルエンディアンだから、のような気はします。つまりビッグエンディアンのアーキテクチャ(SPARCとかPowerPCとか)で Julia が動いてたらもしかしたら結果が変わってしまうかもしれません(情報募集)。 ↩ ↩2 ↩3
-
以前(v0.6.x まで)は、小さいサイズから大きいサイズへの変換の場合、はみ出した値(
int32s=[1,2,3,4,5]
の場合、5番目の要素5
)は切り捨てられてそこより前までで再解釈されていました。v0.7.0/v1.0.0 から、これが許されなくなった模様です。この場合reinterpret(UInt64, int32s[1:4])
のようにすればエラー無く再解釈されます。 ↩ -
Base.isbits 参照 ↩
-
より正確には、ある型のインスタンス
v::T
に対してisbits(v) == true
となる場合、もしくはisbitstype(T) == true
となる場合。詳細は Base.isbitstype 参照。 ↩ -
この条件を満たすとき、その型は参照型ではなく所謂「値型」として扱われる模様です。 某他言語で
class
ではなくstruct
で定義した型が暗黙にValue
を継承した型になるのと似ている、というかたぶん内部で同じことが起きていると思います(未確認)。 ↩ -
ちなみに前節で触れた
Complex{T}
型は、この条件を満たしている Composite Type の例だったりします(T
にあたる型がisbitstype(T)==true
を満たすことが必要)。 ↩ -
7で触れた通り、Julia の内部メモリの扱いがリトルエンディアンであることを想定したコードです。もしビッグエンディアンだったら期待通り動作しない可能性があります(情報募集)。 ↩ ↩2