ここ一か月ほどパソコンが壊れていて修理していたのですが、修理が終わって戻ってきたのでいろいろやっていきます。
バンド端付近にフェルミエネルギーが来る場合、そこの状態密度が高い構造が選ばれる、について
授業で知ったのですが特に証明がなかったので考えました。
仮定
- 構造によらずバンドの上端・下端は同じ
- 構造によらずバンドに入る電子数も同じ(が一定)
- 構造によらず電子がバンドに全部入った時のエネルギーも同じ(が一定)
- 状態密度として上下対称なものしか考えない場合、常にこれが満たされます
フェルミエネルギーがバンド下端付近の場合
少数の電子がバンドに入ることになります。
その場合、バンド下端付近に大きな状態密度を持つ構造のほうが、エネルギーの低い状態にたくさんの電子を詰め込めることになります。
すると全エネルギーが低くなるので、そういった構造のほうが安定で選ばれやすいということになります。
フェルミエネルギーがバンド上端付近の場合
一定のエネルギー準位以下の状態に入れられる電子の数を考えてみます。 すると、バンド上端付近に大きな状態密度を持つ構造は、そうでない構造と比べて少数の電子しか一定のエネルギー準位以下の状態に入れられません。 したがって、バンド上端付近に大きな状態密度を持つ構造は、そうでない構造と比べてフェルミエネルギーが高いです。 それにもかかわらず、全エネルギーはバンド上端付近に大きな状態密度を持つ構造のほうが低くなります。
ちょっと不思議ですが、空孔で考えてみると一目瞭然です。
「構造によらず電子がバンドに全部入った時のエネルギーも同じ」と仮定しているので、「電子が 入らなかった 状態のエネルギーが高ければ高いほど、全エネルギーは低い」ことになります。 するとフェルミエネルギーがバンド下端の時の議論とほぼ同様になり、「バンド上端付近の高いエネルギー準位にたくさんの空孔を詰め込める構造のほうが有利」ということがわかります。
浮動小数点数の比較演算に関するメモ
二つの浮動小数点数を比較する演算は、C言語の演算子としては6種類(<
、<=
、>
、>=
、==
、!=
)ありますが、NaNの取り扱いを考えるともっとあります。
一般的なのは、以下の四状況に関してそれぞれtrue
とfalse
がどうなるかを考えた14種類(24=16のうち、2種類は「常にtrue
」「常にfalse
」で除外)です*1。
- オペランドの少なくとも片方がNaNの場合
- 両方のオペランドがNaNではなく、左オペランドが右オペランドより小さい場合
- 両方のオペランドがNaNではなく、左オペランドが右オペランドより等しい場合(ただし、
0.0
と-0.0
は等しいとみなされる) - 両方のオペランドがNaNではなく、左オペランドが右オペランドより大きい場合
どちらかがNaN | 左<右 | 左>右 | 左=右 | 備考 | |
---|---|---|---|---|---|
OEQ | false |
false |
false |
true |
C言語の== 、RISC-VのFEQ |
OGT | false |
false |
true |
false |
C言語の> |
OGE | false |
false |
true |
true |
C言語の>= |
OLT | false |
true |
false |
false |
C言語の< 、RISC-VのFLT |
OLE | false |
true |
false |
true |
C言語の<= 、RISC-VのFLE |
ONE | false |
true |
true |
false |
|
ORD | false |
true |
true |
true |
|
UNO | true |
false |
false |
false |
|
UEQ | true |
false |
false |
true |
|
UGT | true |
false |
true |
false |
|
UGE | true |
false |
true |
true |
|
ULT | true |
true |
false |
false |
|
ULE | true |
true |
false |
true |
|
UNE | true |
true |
true |
false |
C言語の!= |
C言語の演算子は、!=
以外はorderedな比較を行います。orderedな比較とは、オペランドの少なくとも片方がNaNの場合、false
になるというものです。
そのため、整数の場合と異なり、a > b
とa <= b
の結果がともにfalse
であるということが起こります(a
もしくはb
がNaNの場合に発生します)。
RISC系の命令セットでは、必要最小限の比較命令のみ用意して、後は条件を反転、などの方法で他の比較条件を実現することがあります。 しかし、こういうことがあるので、コンパイラは安易に条件を反転したりできません。 実際にRISC-Vコンパイラ(llc)がどのようにして各条件を実現しているかを調べてみると、以下のようになっていました。
比較 | 実現方法 |
---|---|
OEQ | FEQを使う |
OGT | FLTを使う(オペランド順序を逆にする) |
OGE | FLEを使う(オペランド順序を逆にする) |
OLT | FLTを使う |
OLE | FLEを使う |
ONE | ORDを計算して、FEQの結果を反転したものとANDをとる |
ORD | オペランドごとに自分とのFEQを計算してANDをとる |
UNO | ORDを計算して結果を反転 |
UEQ | UNOを計算して、FEQの結果とORをとる |
UGT | FLEの結果を反転 |
UGE | FLTの結果を反転 |
ULT | オペランド順序を逆にしたFLEの結果を反転 |
ULE | オペランド順序を逆にしたFLTの結果を反転 |
UNE | FEQの結果を反転 |
いくつかはllvmの自動フォールバック(SelectionDAGLegalize::LegalizeSetCCCondCode関数)で生成されているので一部無駄のあるコード生成になっています。 例えば、ONEを計算するにはOGTの結果とOLTの結果をORするのが、命令数の観点から最適と思われます。
実際、(a > b) | (a < b)
をコンパイルしてみると以下の残念なコードが出力されます。
C言語フロントエンドは正しく最適化してllvm-IRにONEを出力するものの、RISC-Vバックエンドはその最適化をうまく活用できなかったようです。
feq.d a0, ft0, ft0 feq.d a1, ft1, ft1 and a0, a0, a1 feq.d a1, ft1, ft0 not a1, a1 and a0, a0, a1
ちなみに、UNOは、llvmの自動フォールバックでは「オペランドごとに自分とのUNEを計算してORをとる」ですが、RISC-Vコンパイラは最適な「オペランドごとに自分とのOEQを計算してANDしたものを反転」を出力します(RISCVInstrInfoD.tdに書かれています)。
gnuplotの等高線でヒートマップを描きたい
二次元データを可視化する方法に、標高線を描くという方法があります。
また、ヒートマップを使う方法もあります。
標高線通りにヒートマップを作ってほしいのですが、gnuplotが標高線を描画するアルゴリズムとヒートマップを作る時の補間アルゴリズムは異なっているため、ずれが発生します。
これを一貫するようにしてみたいと思います。
戦略
標高線は、図1や図3だと直線で補間していますが、スプライン曲線で補間することもできます。 しかし、スプライン曲線で補間しても、ヒートマップとはずれが生じます。
そこで、gnuplotに標高線データをsvgで出力してもらって、それをスクリプトで加工することを考えます。
実装
まずはこんな感じでgnuplotに標高線データをsvgで出力してもらって、
gnuplot -e "set terminal svg; set output 'contour.svg'; set view map; unset surface; set contour base; set xrange [0:4]; set yrange [0:4]; set xtics 1; set ytics 1; set ticscale 0; set cntrparam bspline; set cntrparam levels incremental 0,2,16; splot 'data' u 1:2:3 w l notitle"
require 'rexml/document' palette = ["rgb(96,1,199)", "rgb(136,6,249)", "rgb(167,20,111)", "rgb(193,48,0)", "rgb(216,93,0)", "rgb(236,161,0)", "rgb(255,255,0)"] xml = REXML::Document.new(File.new("contour.svg")) contour = xml.root.get_elements('g/g/g/path').map { |e| e.attribute("d") }.reverse xml.root.elements.each_with_index('g/g/g/path') { |e, i| e.add_attribute("d", contour[i]) e.add_attribute("fill", palette[i]) e.add_attribute("stroke", palette[i]) } File.write("contour.svg", xml)
reverse
でひっくり返しているのは、描画順を入れ替えるためです。
gnuplotは標高線データを値の大きいほうから順番に出力してきます。
この方法でうまくいくためには、少なくとも以下の条件が要求されます。
- 等高線に谷のような部分がなく山だけがある(複数の頂上があるのは大丈夫)
- 標高の低い部分の塗りつぶしから順にキャンバスに書くことで、間が塗りつぶされているように見せているため
- 一周しない等高線の始点と終点は同じ辺に属している
- もともと線だったものを面に変換しているので、一周しない線の場合は始点と終点が結ばれる
- 標高線が切れる部分が同じ辺にある場合は始点と終点が結ばれて問題なくなる
- 標高線が切れる部分が違う辺にある場合は斜めに結ばれて正しくなくなる
結果
作図に用いたコマンド
元データ
0 0 1 0 1 7 0 2 7 0 3 7 0 4 1 1 0 1 1 1 15 1 2 13 1 3 15 1 4 1 2 0 1 2 1 3 2 2 11 2 3 11 2 4 1 3 0 1 3 1 3 3 2 11 3 3 3 3 4 1 4 0 1 4 1 1 4 2 1 4 3 1 4 4 1
使ったgnuplotコマンド
図1
gnuplot -e "set terminal png; set output 'contour.png'; set view map; unset surface; set xrange [0:4]; set yrange [0:4]; set xtics 1; set ytics 1; set ticscale 0; set pm3d at b; set pm3d interpolate 100,100; set cbrange [0:16]; set palette maxcolors 8; splot 'data' u 1:2:3 w l notitle"
図2
gnuplot -e "set terminal png; set output 'contour.png'; set view map; set contour base; unset surface; set xrange [0:4]; set yrange [0:4]; set xtics 1; set ytics 1; set ticscale 0; set cntrparam linear; set cntrparam levels incremental 0,2,16; splot 'data' u 1:2:3 w l notitle"
図3
gnuplot -e "set terminal png; set output 'contour.png'; set view map; set contour base; set xrange [0:4]; set yrange [0:4]; set xtics 1; set ytics 1; set ticscale 0; set cntrparam linear; set cntrparam levels incremental 0,2,16; set pm3d at b; set pm3d interpolate 100,100; set cbrange [0:16]; set palette maxcolors 8; splot 'data' u 1:2:3 w l notitle"
完全精度sincosf関数の作り方(その1)
前に完全精度expf関数を作った(高速な完全精度 expf 関数の作り方 - よーる)ので、今度は完全精度sincosf関数を作っていきます。
作戦
まず、引数の絶対値が小さい時のsin関数とcos関数を(taylor展開などを利用して)作ります。 引数の絶対値が大きい場合、引数をと分解します。その後の値に応じて先に作ったsin関数やcos関数を使い分ければ答えが求まります。
ただし、この分解は簡単にできることではありません。 今回は、ある程度簡単に実行可能な、引数の絶対値がそれほど大きくない(4×108程度以下)場合のみを作ることにします。
ベースとなるTaylor展開ベースの実装
まず、sin関数とcos関数をtaylor展開で求めるコードを書きます。double
精度で求めて最後にfloat
に丸める作戦なので、そこまで精度は必要ではありません。
double cos_taylor(double x) { double x2 = x * x; double a = -1./87178291200; a = std::fma(a, x2, 1./479001600); a = std::fma(a, x2, -1./3628800); a = std::fma(a, x2, 1./40320); a = std::fma(a, x2, -1./720); a = std::fma(a, x2, 1./24); a = std::fma(a, x2, -1./2); return std::fma(a, x2, 1); } double sin_taylor(double x) { double x2 = x * x; double a = -1./1307674368000; a = std::fma(a, x2, 1./6227020800); a = std::fma(a, x2, -1./39916800); a = std::fma(a, x2, 1./362880); a = std::fma(a, x2, -1./5040); a = std::fma(a, x2, 1./120); a = std::fma(a, x2, -1./6); return std::fma(a, x2 * x, x); }
で割ったあまりを求める
引数をと分解するためには、以下のような計算を行います。 これは、完全精度expf関数を作る時に行った、引数をと分解するのと同様の手順です。
constexpr double harf_pi_hi = 0x1.921fb5p+0; constexpr double harf_pi_lo = 0x1.110b4611a6263p-26; constexpr double inv_harf_pi = 0x1.45f306dc9c883p-1; double k = std::fma(x, inv_harf_pi, 0x3.p+51) - 0x3.p+51; assert( -0x1.p+28 <= k && k <= 0x1.p+28 ); double t = std::fma(-harf_pi_lo, k, std::fma(-harf_pi_hi, k, x));
ここで、harf_pi_hi
、harf_pi_lo
、inv_harf_pi
は、以下の条件を満たすdouble
の定数です。
harf_pi_hi
の下位Nビットは0harf_pi_hi
の値とharf_pi_lo
の値を足し合わせると、に近いinv_harf_pi
の値はに近い
harf_pi_hi
の下位Nビットを0にするのは、k
倍したときに誤差が生じないようにするためです。
ここでN=28を選べば、先のコードになります。
N=28の時、x
の絶対値が4.2e+8
程度以下であればassert
が満たされます。
なぜN=28を選んだか
なぜN=28を選ぶ必要があるのでしょうか。例えば、Nをもっと大きくすれば、対応できるx
の範囲が広がりよさそうに思われます。
しかし、実際には、Nを大きくするとの有効桁数が小さくなり、それに起因する誤差が増加し、完全精度を達成できなくなります。
このことを説明してみます。
double
の精度は53ビットですが、二つのdouble
の和で表された数の精度は107ビットです。これは、下位を担当するdouble
の符号ビットも有効活用されるためです。
ここで、Nビットを強制的に0にしなければならないので、の実効的な精度は107-Nビットです。
を計算するとき、桁落ちが発生します。ととで打ち消しあう部分の長さはおおよそN+1ビットなので、最終結果には106-2Nビットしか残りません。
N=29の場合、小数部には48ビットしか使われないということになります。これはdouble
の精度を下回っています。
実際、0x1.8db252p+25
をで割ったあまりを求めてみると、正確には0x1.5292b563fb139p-5
程度になるべきところ、0x1.5292b563fb120p-5
と求まってしまいます。なんと25ULPも誤差が発生しています。
運の悪いことにcos(0x1.8db252p+25) = 0x1.527a08ffffff0p-5
とfloat
に丸めるときの丸め境界から16ULP程度しか離れていないため、ここの計算を間違えてしまいます。
よって、このような問題を発生させないためにも、N=28よりも大きくすることはできません(N=28であっても、運よくこのような問題が発生していないだけであり、やや精度不足であることには変わりありません)。
絶対値がそれほど大きくない場合の実装
void sincos_reduction(double x, double* s, double* c) { constexpr double harf_pi_hi = 0x1.921fb5p+0; constexpr double harf_pi_lo = 0x1.110b4611a6263p-26; constexpr double inv_harf_pi = 0x1.45f306dc9c883p-1; double k = std::fma(x, inv_harf_pi, 0x3.p+51) - 0x3.p+51; assert( -0x1.p+28 <= k && k <= 0x1.p+28 ); double t = std::fma(-harf_pi_lo, k, std::fma(-harf_pi_hi, k, x)); std::int32_t k_ = static_cast<std::int32_t>(k); switch(k_ & 3) { case 0: *s = sin_taylor(t), *c = cos_taylor(t); break; case 1: *s = cos_taylor(t), *c = -sin_taylor(t); break; case 2: *s = -sin_taylor(t), *c = -cos_taylor(t); break; case 3: *s =-cos_taylor(t), *c = sin_taylor(t); break; } }
8/28 追記
入力が±0x1.33333p+13
の時、sin(x) ~ ∓0x1.63f4bafffffffa5cp-2
であり、完全精度なら∓0x1.63f4bap-2
を返すべきですが、上記コードは∓0x1.63f4bcp-2
を返すので完全精度になっていませんでした。
これはテイラー展開の誤差由来やリダクション部分の誤差由来ではなく、double
を経由していること、つまり二回丸めていることが原因でした。sin(x) ~ ∓0x1.63f4bafffffffa5cp-2
はdouble
に丸めると∓0x1.63f4bb0000000p-2
に丸められ、float
の丸め境界上の値に丸まってしまいます。これをfloat
に丸めると偶数丸めになるため最終桁が0になる∓0x1.63f4bcp-2
に丸められ、正しくなくなります。
RISC-Vのビット操作系拡張(B拡張)のまとめ(その5)
ブール環(、、“足し算”がxorで“掛け算”がandであるような演算体系、“2で割ったあまり”)に関係する命令群です。
clmul
, clmulh
, clmulr
繰上りなしの掛け算です。ビット列の畳み込みと考えることもできます。
clmul
は掛け算の結果の下32/64ビットを返します。
clmulh
は掛け算の結果の上32/64ビット(下から32~63/64~127ビット目)を返します。
ここで、繰上りがないため、clmulh
の最上位ビットは常に0です。
clmulr
はその再上位ビットを除いた上32/64ビット(下から31~62/63~126ビット目)を返します。
名前のrは、reverseに由来します。clmulr
の結果は、ビット順序逆転した結果同士のclmul
の結果のビット順序逆転と同じになるからです。
巡回冗長検査(Cyclic Redundancy Check, CRC)やハッシュ関数の実装などに有用だそうです。
自分自身とのclmul
/clmulh
を使うと、0を00に、1を01に変換する(32/64ビットのビット列の各ビットの間に0を挿入して64/128ビットのビット列にする)操作を実現します。
また、-1
とのclmul
はprefix sum(ここで言うsumはxor)を求めるのに使えます(グレイコードのデコードに使えます)。
crc32
, crc32c
巡回冗長検査(CRC)をまさにそのまま計算する命令です。
bmatxor
, bmator
, bmatflip
64bitのレジスタを、8×8のビット行列だとみなして、行列演算を行う命令です。
bmatxor
は、“足し算”がxorであるような行列乗算、bmator
は“足し算”がorであるような凝結乗算、bmatflip
は行列転置です。
bmatflip
は、zip
三回分に相当します。
RISC-Vのビット操作系拡張(B拡張)のまとめ(その4)
ビットフィールドやビットベクトルの操作系命令をまとめます。
bfp
rs1
のoff
ビット目からlen
ビットをrs2
の下len
ビットに置き換えます。
ここで、off
とlen
は(オペランド数の都合上)rs2
の上位ビットを使って指定します。
off
は、RV32であればrs2
の16ビット目から20ビット目、RV64であればrs2
の32ビット目から37ビット目または48ビット目から53ビット目を使います。
len
は、RV32であればrs2
の24ビット目から27ビット目、RV64であればrs2
の40ビット目から44ビット目または56ビット目から60ビット目を使います。
「または」と書いてある部分は、rs2
の62ビット目から63ビット目が2
であれば後者を、そうでなければ前者を使います。
len
は1から16/32になるはずです。そこで、4/5bitのフィールドは、すべてが0の時に16/32と解釈する工夫がなされます。
off
やlen
を指定する定数をrs2
の上位ビットに送り込むには、pack
命令が有用です。
RV32の場合、addi
で作った12ビットの定数をpack
命令で上位ビットに送り込めば、任意のlen
とoff
を実現できます。
RV64の場合、lui
で作った32ビットの定数をpack
命令で上位ビットに送り込めば、任意のlen
とoff
を実現できます(「または」と書いてある部分を後者で使うモードを利用します)。
bext
, bdep
bext
命令は、rs1
のうち、rs2
の中で1がたっているビットの部分だけを下位ビットに集約します。
bdep
命令は、その逆操作を行います。つまり、rs2
の中で1がたっているビットの位置に、rs1
の各ビットを展開します。
bext
命令を使うと、Magic Bitboardみたいなことが簡単にできます。
また、bdep
命令を使うと簡潔データ構造で用いられるようなselect1(rank)
演算が実現可能です。先頭から10ビット目の1の位置を知りたい場合、bdep a0, 1ull<<10, a0; ctz a0, a0
のようにすることで求まります。