Java視点からシステム構造を理解する(一)CPUコンテキスト切替


本文はJavaの視点からシステム構造を理解する連載文章である.
高性能プログラミングでは、マルチスレッドによく接触します.最初は、複数のスレッドが単一のスレッドよりも並列に実行されるのが速く、複数のスレッドが一人で作業するよりも速くなります.しかし、実際には、マルチスレッド間でIOデバイスを競合したり、リソースを競合したりする必要があります.実行速度が単一スレッドに及ばないことが多い.ここでよく言及する概念は,コンテキスト切替(Context Switch)である.
コンテキスト切り替えの正確な定義は、次のように参照できます.http://www.linfo.org/context_switch.html.以下に简単な绍介をします.マルチタスクシステムは往々にして同时にマルチタスクを実行する必要があります.作业数は往々にして机械のCPU数より大きいですが、1つのCPUは同时に1つのタスクしか実行できません.どのようにユーザーにこれらのタスクが同时に行われていると感じさせますか?オペレーティングシステムの设计者は巧みにタイムスライスの回転の方式を利用して、CPUはすべての任务に一定の时间サービスして、それから现在の任务の状态を保存して、次の任务の状态をロードした后で、引き続き次の任务にサービスします.任务の状态は保存して更にロードして、このプロセスをコンテキスト切替と呼ぶ.タイムスライスの回転方式は複数のタスクを同じCPU上で実行することを可能にしたが,同時に現場の保存と現場のロードの直接的な消費をもたらした.
(Note.より正確に言えば、コンテキストの切り替えは直接と間接の2つの要素がプログラムの性能の消耗に影響することをもたらす.直接消耗は:CPUレジスタは保存とロードを必要とし、システムスケジューラのコードは実行を必要とし、TLBインスタンスは再ロードを必要とし、CPUのpipelineはブラシを必要とする;間接消耗はマルチコアのcacheの間でデータを共有し、間接消耗はプログラムに対する影響を必要とするスレッドワークスペースの操作データの大きさを見る).
linuxではvmstatを使用してコンテキスト切替の回数を観察できます.実行コマンドは次のとおりです.
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa
 1  0      0 4593944 453560 1118192    0    0    14    12  238   30  6  1 92  1
 0  0      0 4593212 453568 1118816    0    0     0    96  958 1108  4  1 94  2
 0  0      0 4593360 453568 1118456    0    0     0     0  895 1044  3  1 95  0
 1  0      0 4593408 453568 1118456    0    0     0     0  929 1073  4  1 95  0
 0  0      0 4593496 453568 1118456    0    0     0     0 1133 1363  6  1 93  0
 0  0      0 4593568 453568 1118476    0    0     0     0  992 1190  4  1 95  0
vmstat 1は、cs列がコンテキスト切替の数を指す毎秒統計を指す.一般的に、アイドルシステムのコンテキスト切替は毎秒1500以下である.
私たちがよく使うプリエンプトオペレーティングシステムにとって、コンテキストの切り替えを引き起こす原因は以下のいくつかあります:1.現在タスクを実行するタイムスライスが切れた後、システムCPUは正常に次のタスクをスケジューリングします.2.現在の実行タスクがIOブロックにぶつかって、スケジューラはこのタスクを掛けて、次のタスクを続けます.3.複数のタスクがリソースをプリエンプトロックして、現在のタスクはプリエンプトしていません.スケジューラにより保留され、次のタスク4に進む.ユーザコードは現在のタスクを保留し、CPU時間5.ハードウェアを中断させる.
この間、futexのWAITとWAKEを使ってcontext switchの直接消費(リンク)をテストしている人もいれば、ブロックIOを使ってcontext switchの消費(リンク)をテストしている人もいました.Javaプログラムはどのようにコンテキスト切替の消費をテストし、観察していますか.
私は小さな実験をしました.コードは簡単で、2つの作業スレッドがあります.最初は、最初のスレッドが自分を掛けました.2番目のスレッドは1番目のスレッドを呼び覚まし、自分を掛けます.1番目のスレッド目が覚めたら2番目のスレッドを起こして、自分を掛けます.このように行き来して、お互いに相手を起こして、自分を掛けます.コードは以下の通りです.
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;

public final class ContextSwitchTest {
    static final int RUNS = 3;
    static final int ITERATES = 1000000;
    static AtomicReference turn = new AtomicReference();

    static final class WorkerThread extends Thread {
        volatile Thread other;
        volatile int nparks;

        public void run() {
            final AtomicReference t = turn;
            final Thread other = this.other;
            if (turn == null || other == null)
                throw new NullPointerException();
            int p = 0;
            for (int i = 0; i < ITERATES; ++i) {
                while (!t.compareAndSet(other, this)) {
                    LockSupport.park();
                    ++p;
                }
                LockSupport.unpark(other);
            }
            LockSupport.unpark(other);
            nparks = p;
            System.out.println("parks: " + p);

        }
    }

    static void test() throws Exception {
        WorkerThread a = new WorkerThread();
        WorkerThread b = new WorkerThread();
        a.other = b;
        b.other = a;
        turn.set(a);
        long startTime = System.nanoTime();
        a.start();
        b.start();
        a.join();
        b.join();
        long endTime = System.nanoTime();
        int parkNum = a.nparks + b.nparks;
        System.out.println("Average time: " + ((endTime - startTime) / parkNum)
                + "ns");
    }

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < RUNS; i++) {
            test();
        }
    }
}

コンパイル後、自分のノート(Intel(R)Core(TM)i 5 CPU M [email protected] GHz、2 core、3 M L 3 Cache)で数回テストした結果、以下のようになりました.
java -cp . ContextSwitchTest
parks: 953495
parks: 953485
Average time: 11373ns
parks: 936305
parks: 936302
Average time: 11975ns
parks: 965563
parks: 965560
Average time: 13261ns

このような簡単なforサイクルは、線形実行が非常に速く、1秒を必要としないが、このプログラムを実行するには数十秒の時間がかかることが分かった.コンテキスト切替ごとに十数usの時間がかかり、プログラムのスループットに大きな影響を及ぼす.
同時にvmstat 1を実行してコンテキスト切替の頻度が速くなるかどうかを確認することができます.
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa
 1  0      0 4424988 457964 1154912    0    0    13    12  252   80  6  1 92  1
 0  0      0 4420452 457964 1159900    0    0     0     0 1586 2069  6  1 93  0
 1  0      0 4407676 457964 1171552    0    0     0     0 1436 1883  8  3 89  0
 1  0      0 4402916 457964 1172032    0    0     0    84 22982 45792  9  4 85  2
 1  0      0 4416024 457964 1158912    0    0     0     0 95382 198544 17 10 73  0
 1  1      0 4416096 457964 1158968    0    0     0   116 79973 159934 18  7 74  0
 1  0      0 4420384 457964 1154776    0    0     0     0 96265 196076 15 10 74  1
 1  0      0 4403012 457972 1171096    0    0     0   152 104321 213537 20 12 66  2

さらにstraceを使用して、上記のプログラムのUnsafe.park()がどのシステム呼び出しでコンテキストを切り替えたのかを観察します.
$strace -f java -cp . ContextSwitchTest
[pid  5969] futex(0x9571a9c, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x9571a98, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
[pid  5968]  )       = 0
[pid  5969] futex(0x9571ad4, FUTEX_WAIT_PRIVATE, 949, NULL
[pid  5968] futex(0x9564368, FUTEX_WAKE_PRIVATE, 1) = 0
[pid  5968] futex(0x9571ad4, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x9571ad0, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}
[pid  5969]  )       = 0
[pid  5968]  )       = 1
[pid  5969] futex(0x9571628, FUTEX_WAIT_PRIVATE, 2, NULL

やっぱりフテクス.
perfを使用してCacheに及ぼすコンテキストの影響を確認します.
$ perf stat -e cache-misses   java -cp . ContextSwitchTest
parks: 999999
parks: 1000000
Average time: 16201ns
parks: 998930
parks: 998926
Average time: 14426ns
parks: 998034
parks: 998204
Average time: 14489ns

 Performance counter stats for 'java -cp . ContextSwitchTest':

         2,550,605 cache-misses

      90.221827008 seconds time elapsed

1分半以内に255万回以上のcacheが命中しなかった.
ええと、長すぎて終わりそうです.これからもいくつかのブログを続けて面白いものを分析していきます.(1)Java視点からメモリバリア(Memory Barrier)(2)java視点からCPUの親縁性(CPU Affinity)などを見てみましょう.
PS.実はもう一つの実験を行い、CPU AffinityがContext Switchに与える影響をテストした.
$ taskset -c 0 java -cp . ContextSwitchTest
parks: 992713
parks: 1000000
Average time: 2169ns
parks: 978428
parks: 1000000
Average time: 2196ns
parks: 989897
parks: 1000000
Average time: 2214ns

このコマンドはプロセスを0番CPUにバインドした結果、Context Switchの消費量が1桁小さくなったのはなぜですか?関系を売って、CPU Affinityの博文について話してから言います:).
by Minzhou via ifeve