「単純和」プログラムの速度をJuliaとC言語で比較した

先週、浮動小数点数を足していくプログラムについて、Juliaで書いた場合とC言語で書いた場合とで速度を比較しました(浮動小数点数を足すだけのプログラムがC言語よりJuliaのほうが速い、について - よーる)。

この記事について、黒木玄さん(黒木玄 Gen Kuroki (@genkuroki) | Twitter)から「Juliaでは単純な和には普通にsimdを使うので、JuliaとCの両方でsimdを使って比較した方が良かったと思います。」というコメントをいただきました。 先週の記事で比較に用いたプログラムには並列性がないため、SIMD (single instruction multiple data) 命令を使った高速化は不可能なはずですが、Juliaでsimdを使うと10倍高速化するそうです。 一体どういうことなのだろうと思ってJuliaのマニュアル(Base.SimdLoop.@simd (macro))を見てみると、@simdというアノテーション浮動小数点数演算の順序を変える効果があるそうです。

浮動小数点数演算は一般に結合則が成り立ちません。 したがって、浮動小数点演算の順序を変えてしまうと、プログラムの意味が変わってしまいます(一般に計算結果も変わってしまいます)。 先週の記事で比較したプログラムはあくまで「浮動小数点数を前から順番に足していく」プログラムであって、「いい感じに浮動小数点数の和を求める」プログラムではありません。

この辺は、多くの読者が勘違いされているところだと感じました。

とはいえ、「浮動小数点数を前から順番に足していった値を正確に求めたい」というワークロードが常識的ではないのも事実です。 そこで、「浮動小数点数を順番を気にせずに足した値を求める」というワークロードで、JuliaとC言語の速度比較をしてみます。

使ったソースコード

ちょうど黒木玄さんがJulia側のソースコードを示してくださいました(simd example · GitHub)ので、それを使うことにします*1。 ただし、実行時間が短すぎて正確な評価ができなかったので、外側ループの回数を100倍にしました。

C言語側のソースコードは、私が素直に移植したものになります。

function test_inbounds_simd()
    a = fill(1e-10, 1000)
    s = 0.0
    for j = 1:1000000000
        @inbounds @simd for i = 1:1000
            s += a[i]
        end 
    end
    s
end
test_inbounds_simd()
double test() {
  unsigned long long i, j;
  double a[1000];
  double s = 0.0;
  for( i = 0; i < 1000; ++i ) { a[i] = 1e-10; }

  for( j = 0; j < 1000000000; ++j ) {
    for( i = 0; i < 1000; ++i ) {
      s += a[i];
    }
  }

  return s;
}

int main() {
  return test();
}

使用したハードウェア

  • Intel(R) Xeon(R) Platinum 8280 CPU @ 2.70GHz
    • 実際には3.6GHzくらいで動いているっぽい(?)
    • AVX-512使用可能(一命令で倍精度浮動小数点数8個を同時に演算可能)
    • FMA実行ユニット2基(1cycleに浮動小数点数加算命令が2回実行できる)
    • 浮動小数点数加算命令のレイテンシは4cycle

使用したソフトウェア

  • julia version 1.5.2(内部で使用しているLLVMのバージョンはLLVM version 9.0.1jl
  • clang version 10.0.0
  • icc (ICC) 19.0.5.281 20190815
  • perf
  • gdb

理論限界

ベンチマークプログラムは1000G個の浮動小数点数を加算するものです。 実験に使用したプロセッサは1サイクルに16個の浮動小数点数を加算することができますので、理論限界は62.5Gサイクルです。 3.6GHzで動いているとすると17秒程度かかることになりますが、周波数が安定しないので実時間は参考程度ということにします。

Juliaの結果

  • 199G cycles
  • 278G insns
  • 43G branches
  • 55秒くらい

JITコンパイルされた機械語gdbで確認してみると、以下のようになっていました。

   0x00007fffc565f0d0:  cmp    rax,0x3b9aca00
   0x00007fffc565f0d6:  lea    rax,[rax+0x1]
   0x00007fffc565f0da:  je     0x7fffc565f187
   0x00007fffc565f0e0:  vmovq  xmm0,xmm0
   0x00007fffc565f0e4:  vxorpd xmm1,xmm1,xmm1
   0x00007fffc565f0e8:  mov    esi,0x18
   0x00007fffc565f0ed:  vxorpd xmm2,xmm2,xmm2
   0x00007fffc565f0f1:  vxorpd xmm3,xmm3,xmm3
   0x00007fffc565f0f5:  nop    WORD PTR cs:[rax+rax*1+0x0]
   0x00007fffc565f0ff:  nop
   0x00007fffc565f100:  vaddpd zmm0,zmm0,ZMMWORD PTR [rcx+rsi*8-0xc0]
   0x00007fffc565f108:  vaddpd zmm1,zmm1,ZMMWORD PTR [rcx+rsi*8-0x80]
   0x00007fffc565f110:  vaddpd zmm2,zmm2,ZMMWORD PTR [rcx+rsi*8-0x40]
   0x00007fffc565f118:  vaddpd zmm3,zmm3,ZMMWORD PTR [rcx+rsi*8]
   0x00007fffc565f11f:  add    rsi,0x20
   0x00007fffc565f123:  cmp    rsi,0x3f8
   0x00007fffc565f12a:  jne    0x7fffc565f100
   0x00007fffc565f12c:  vaddpd zmm0,zmm1,zmm0
   0x00007fffc565f132:  vaddpd zmm0,zmm2,zmm0
   0x00007fffc565f138:  vaddpd zmm0,zmm3,zmm0
   0x00007fffc565f13e:  vextractf64x4 ymm1,zmm0,0x1
   0x00007fffc565f145:  vaddpd zmm0,zmm0,zmm1
   0x00007fffc565f14b:  vextractf128 xmm1,ymm0,0x1
   0x00007fffc565f151:  vaddpd xmm0,xmm0,xmm1
   0x00007fffc565f155:  vpermilpd xmm1,xmm0,0x1
   0x00007fffc565f15b:  vaddsd xmm0,xmm0,xmm1
   0x00007fffc565f15f:  test   dl,dl
   0x00007fffc565f161:  jne    0x7fffc565f0d0
   0x00007fffc565f167:  xor    esi,esi
   0x00007fffc565f169:  nop    DWORD PTR [rax+0x0]
   0x00007fffc565f170:  vaddsd xmm0,xmm0,QWORD PTR [rcx+rsi*8+0x1f00]
   0x00007fffc565f179:  inc    rsi
   0x00007fffc565f17c:  cmp    rsi,0x8
   0x00007fffc565f180:  jne    0x7fffc565f170
   0x00007fffc565f182:  jmp    0x7fffc565f0d0

この機械語コードから予測すると、1000個の浮動小数点数を足すのに

  • 192cycle(zmmレジスタを使った主ループが4cycle×31回、マージしていく部分が36cycle、端数処理ループが4cycle×8回)
  • 273命令(準備に11命令、主ループが7命令×31回、マージしていく部分が13命令、端数処理ループが4命令×8回)
  • 42分岐命令

が必要です。

これはperfコマンドの結果と整合します。

理論限界と大きく差がついている理由は、以下の通りです。

  • 主ループのアンローリングが不足している
    • 4cycleレイテンシを隠蔽しつつFMAユニット2基をフル稼働させるためには、依存関係にない命令が最低でも8つ必要
  • 端数処理が直列化されている

C言語の結果(clangでコンパイルした場合)

コンパイルオプションは、-O2 -mavx512f -funsafe-math-optimizationsです*2C言語の範囲では「浮動小数点数演算の順序を自由に変更してかまわない」という記述をすることができないので、-funsafe-math-optimizationsをつけます*3*4

  • 194G cycles
  • 213G insns
  • 32G branches
  • 55秒くらい

Juliaで書いた場合と同じくらいの速度になりました。それもそのはずで、JuliaのJITエンジンはLLVM、つまりclangのバックエンドと同じです。 というわけでコード生成結果や実行速度が大きく変わるはずはないのです。

命令数が少なくなっているのは、JITコンパイルより時間をかけてコンパイルすることできれいなコードを生成できたことによるものと思われます。

.LBB0_5:
        vaddpd  zmm0, zmm2, zmm0
        vaddpd  zmm0, zmm3, zmm0
        vaddpd  zmm0, zmm4, zmm0
        vextractf64x4   ymm2, zmm0, 1
        vaddpd  zmm0, zmm0, zmm2
        vextractf128    xmm2, ymm0, 1
        vaddpd  xmm0, xmm0, xmm2
        vpermilpd       xmm2, xmm0, 1
        vaddsd  xmm0, xmm0, xmm2
        vaddsd  xmm0, xmm0, xmm1
        vaddsd  xmm0, xmm0, xmm1
        vaddsd  xmm0, xmm0, xmm1
        vaddsd  xmm0, xmm0, xmm1
        vaddsd  xmm0, xmm0, xmm1
        vaddsd  xmm0, xmm0, xmm1
        vaddsd  xmm0, xmm0, xmm1
        vaddsd  xmm0, xmm0, xmm1
        add     rax, 1
        cmp     rax, 1000000000
        je      .LBB0_6
.LBB0_3:
        vmovq   xmm0, xmm0
        vxorpd  xmm2, xmm2, xmm2
        mov     ecx, 56
        vxorpd  xmm3, xmm3, xmm3
        vxorpd  xmm4, xmm4, xmm4
        .p2align        4, 0x90
.LBB0_4:
        vaddpd  zmm0, zmm0, zmmword ptr [rsp + 8*rcx - 576]
        vaddpd  zmm2, zmm2, zmmword ptr [rsp + 8*rcx - 512]
        vaddpd  zmm3, zmm3, zmmword ptr [rsp + 8*rcx - 448]
        vaddpd  zmm4, zmm4, zmmword ptr [rsp + 8*rcx - 384]
        cmp     rcx, 1016
        je      .LBB0_5
        vaddpd  zmm0, zmm0, zmmword ptr [rsp + 8*rcx - 320]
        vaddpd  zmm2, zmm2, zmmword ptr [rsp + 8*rcx - 256]
        vaddpd  zmm3, zmm3, zmmword ptr [rsp + 8*rcx - 192]
        vaddpd  zmm4, zmm4, zmmword ptr [rsp + 8*rcx - 128]
        add     rcx, 64
        jmp     .LBB0_4

zmmレジスタを使った主ループが二倍アンローリングされたほか、端数ループが八倍(完全)アンローリングされています。これらが命令数削減に役立っているようです。

この機械語コードから予測すると、1000個の浮動小数点数を足すのに

  • 192cycle(zmmレジスタを使った主ループが8cycle×15.5回、マージしていく部分が36cycle、端数処理が4cycle×8回)
  • 213命令(ループの外が27命令、ループの中が12命令×15.5回)
  • 32分岐命令

が必要です。

これはperfコマンドの結果と整合します。

理論限界と大きく差がついている理由は、Juliaの場合と全く同様です。

どうでもいいですが、配列の最後のほうがループ内定数とみなされてxmmレジスタに乗っていますね……。これは微妙にずるな気がしますが計算時間や命令数に変わりはないのでよしとしましょう。

C言語の本気(iccコンパイルした場合)

C言語の強みは何といっても ハードウェアを作っている会社が専用コンパイラを提供してくれる ことにあります。

Intel社が作ったコンパイラiccを使ってコンパイルしてみましょう。 コンパイルオプションは、-axCORE-AVX512 -qopt-zmm-usage=highです。

結果は以下の通りです。

  • 62.7G cycles
  • 172G insns
  • 15.6G branches
  • 18秒くらい

理論限界の62.5Gサイクルにかなり近い速度が達成できていることがわかります*5

最内ループを確認してみると、お互いに依存しない8個のvaddpd命令になっていました。 さすがにこの辺の最適化はちゃんとしていますね。

まとめ

*1:この手の言語間の速度比較では、不適切なソースコードが使われることにより特定の言語の性能が不当に低く出てしまうということがありがちですが、そういうことにならない有意義な比較ができることを感謝いたします。

*2:-O3や-Ofastだと完全にアンローリングされてしまって見づらかったので-O2にしました(実行にかかるサイクル数は同じでした)。

*3:-funsafe-math-optimizationsは-ffast-mathに含まれるので、-ffast-mathでもよいです。

*4:-funsafe-math-optimizationsは複数のオプションを有効にしますが、実際に役立っているのは-fassociative-math -fno-signed-zerosのようです。

*5:ループ交換が行われているので本来とは全く異なる順序で計算されていますが、加算は1000G回行われています。

浮動小数点数を足すだけのプログラムがC言語よりJuliaのほうが速い、について

なんかすごく前に話題になっていたので、確かめてみました。

数値計算に強いプログラミング言語と言えば従来FortranC言語でしたが、近年Pythonのように書けて実用上十分な速度を達成できるJuliaが人気を集めているようです*1

Juliaは動的言語ですが、(1) 型推論を行うためfor文などでいちいち型検査するPythonより速い (2) JITコンパイルされるので関数に切り出した場合に高速、といった点が高速化に寄与しています。

C言語で書いたほうが速いに決まっている」などの決めつけはよくないので、実際に計測してみます。

そもそもベンチマークソースコードが提示されていないので議論のしようがないのですが、「浮動小数点数を足すだけ」という文章を私が解釈したコードは以下の通りになります。

function test()
  a = zeros(Float64, 1000)
  a[1] = 1e-16

  sum = 1.0

  for j = 1:10000000, i = 1:1000
    sum += a[i]
  end

  return sum
end

test()
double test() {
  double a[1000] = {};
  a[0] = 1e-16;

  double sum = 1.0;

  for( unsigned long long j = 0; j < 10000000; ++j ) {
    for( unsigned long long i = 0; i < 1000; ++i ) {
      sum += a[i];
    }
  }

  return sum;
}

int main() {
  return test();
}

メモリアクセスの速度が測りたいわけではないので、配列のサイズはL1キャッシュに乗る程度の大きさとしました。

環境

  • gcc 9.1.0、-O3をつけてコンパイル
  • julia version 1.5.2
  • Intel(R) Xeon(R) CPU E5-2603 v3 @ 1.60GHz(addsd命令は3cycleレイテンシ)

結果

C言語

  • 30.1G cycles
  • 35.1G instructions
  • 5.0G branches
  • 18.8 seconds

Julia

  • 31.9G cycles
  • 61.5G instructions
  • 20.3G branches
  • 19.1 seconds

考察

数値上はJuliaのほうがほんのわずかに遅くなっていますが、C言語は事前コンパイルが必要でそれに0.2秒ほどかかることを考慮に入れると速度は同じと言ってよいでしょう。

そもそもこのベンチマークの速度はレイテンシに支配されているため、それなりの最適化がかかっていれば速度が変わるわけはないのです。

C言語コードをコンパイルした結果は以下のようになっており、ループ一周は7命令です。アセンブリ上でのループ一周は元のソースコードの二周分に相当しています(アンローリングされています)。 この中には直列に依存したaddsd命令が2つあるので、ループ一周当たり6cycleかかることになります。 C言語コンパイルした結果のperfのサイクル数、命令数、分岐命令数は、これらと整合します。

.L3:
        movsd   xmm1, QWORD PTR [rax]
        add     rax, 16
        addsd   xmm1, xmm0
        movsd   xmm0, QWORD PTR [rax-8]
        addsd   xmm0, xmm1
        cmp     rdx, rax
        jne     .L3

一方Juliaの方はサイクル数こそほぼ一緒ですが、命令数がかなり多くなっています。 これは配列の境界検査を毎回行っているためです。 C言語と条件をそろえ、配列境界検査を行わないようにするため、@inboundsをつけてみると、以下のように命令数と分岐命令数が減ります。

  • 31.9G cycles
  • 41.5G instructions
  • 10.3G branches
  • 19.1 seconds

命令数は一ループ当たり2命令、分岐命令数は一ループ当たり1命令減りました。この減った分は、比較命令と分岐命令ということになります。

分岐命令数から察するにループアンローリングは行われていないようですが、Juliaで書いたコードをJITコンパイルした結果はほぼ最適です。

まとめ

浮動小数点数を足すだけのプログラム」の速度がC言語で書いた場合とJuliaで書いた場合で変わることはありませんでした。

*1:ほかの言語では数値計算ができないとか遅いとか言っているわけではありませんが、あまり見かけないですね。最近はC++で書かれたソフトウェアもちらほら見かけ始めました(知っているところではALPSとか)。

バンド端付近にフェルミエネルギーが来る場合、そこの状態密度が高い構造が選ばれる、について

授業で知ったのですが特に証明がなかったので考えました。

仮定

  • 構造によらずバンドの上端・下端は同じ
  • 構造によらずバンドに入る電子数も同じ( N = \int f(E) \, dEが一定)
  • 構造によらず電子がバンドに全部入った時のエネルギーも同じ( U = \int Ef(E) \, dEが一定)
    • 状態密度として上下対称なものしか考えない場合、常にこれが満たされます

フェルミエネルギーがバンド下端付近の場合

少数の電子がバンドに入ることになります。

その場合、バンド下端付近に大きな状態密度を持つ構造のほうが、エネルギーの低い状態にたくさんの電子を詰め込めることになります。

すると全エネルギーが低くなるので、そういった構造のほうが安定で選ばれやすいということになります。

フェルミエネルギーがバンド上端付近の場合

一定のエネルギー準位以下の状態に入れられる電子の数を考えてみます。 すると、バンド上端付近に大きな状態密度を持つ構造は、そうでない構造と比べて少数の電子しか一定のエネルギー準位以下の状態に入れられません。 したがって、バンド上端付近に大きな状態密度を持つ構造は、そうでない構造と比べてフェルミエネルギーが高いです。 それにもかかわらず、全エネルギーはバンド上端付近に大きな状態密度を持つ構造のほうが低くなります。

ちょっと不思議ですが、空孔で考えてみると一目瞭然です。

「構造によらず電子がバンドに全部入った時のエネルギーも同じ」と仮定しているので、「電子が 入らなかった 状態のエネルギーが高ければ高いほど、全エネルギーは低い」ことになります。 するとフェルミエネルギーがバンド下端の時の議論とほぼ同様になり、「バンド上端付近の高いエネルギー準位にたくさんの空孔を詰め込める構造のほうが有利」ということがわかります。

浮動小数点数の比較演算に関するメモ

二つの浮動小数点数を比較する演算は、C言語演算子としては6種類(<<=>>===!=)ありますが、NaNの取り扱いを考えるともっとあります。

一般的なのは、以下の四状況に関してそれぞれtruefalseがどうなるかを考えた14種類(24=16のうち、2種類は「常にtrue」「常にfalse」で除外)です*1

どちらかが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 > ba <= 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に書かれています)。

*1:ほかにもオペランドとしてquiet NaNが与えられた場合に例外を発生させるかを考慮に入れるとさらに状況が二倍に増えます

gnuplotの等高線でヒートマップを描きたい

二次元データを可視化する方法に、標高線を描くという方法があります。

f:id:lpha_z:20200823223749p:plain
図1: 標高線を使った可視化

また、ヒートマップを使う方法もあります。

f:id:lpha_z:20200823223248p:plain
図2: ヒートマップを使った可視化

標高線通りにヒートマップを作ってほしいのですが、gnuplotが標高線を描画するアルゴリズムとヒートマップを作る時の補間アルゴリズムは異なっているため、ずれが発生します。

f:id:lpha_z:20200823224526p:plain
図3: 標高線とヒートマップを同時に使った可視化

これを一貫するようにしてみたいと思います。

戦略

標高線は、図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"

次に以下のRubyスクリプトで加工します。

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は標高線データを値の大きいほうから順番に出力してきます。

この方法でうまくいくためには、少なくとも以下の条件が要求されます。

  • 等高線に谷のような部分がなく山だけがある(複数の頂上があるのは大丈夫)
    • 標高の低い部分の塗りつぶしから順にキャンバスに書くことで、間が塗りつぶされているように見せているため
  • 一周しない等高線の始点と終点は同じ辺に属している
    • もともと線だったものを面に変換しているので、一周しない線の場合は始点と終点が結ばれる
    • 標高線が切れる部分が同じ辺にある場合は始点と終点が結ばれて問題なくなる
    • 標高線が切れる部分が違う辺にある場合は斜めに結ばれて正しくなくなる

結果

f:id:lpha_z:20200823231713p:plain
図4: 標高線と一貫したヒートマップによる可視化

作図に用いたコマンド

元データ

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"