41

「アセンブラー言語」 (GNU をアセンブラーとして使用する x86 アーキテクチャー上の Linux) を学習している間、あははの瞬間の 1 つは、システム コールを使用する可能性でした。これらのシステム コールは非常に便利で、プログラムがユーザー空間で実行されるときに必要になることもあります。
ただし、システム コールは割り込み (およびもちろんシステム コール) を必要とするため、パフォーマンスの点でかなり高価です。つまり、ユーザー空間で現在アクティブなプログラムからカーネル空間で実行されているシステムへのコンテキスト スイッチを行う必要があります。

私が言いたいことは次のとおりです。現在、(大学のプロジェクト用に) コンパイラを実装していますが、追加したい機能の 1 つは、コンパイルされたプログラムのパフォーマンスを向上させるためのマルチスレッド コードのサポートです。 . マルチスレッド コードの一部はコンパイラ自体によって自動的に生成されるため、これにより、マルチスレッド コードのごく一部が含まれることがほぼ保証されます。パフォーマンスを向上させるには、スレッドを使用することでこれが実現することを確認する必要があります。

ただし、スレッドを使用するには、システム コールと必要な割り込みを行わなければならないのではないかと心配しています。したがって、非常に小さな (自動生成された) スレッドは、これらのシステム コールを実行するのにかかる時間の影響を大きく受け、パフォーマンスの低下につながる可能性さえあります...

したがって、私の質問は2つあります(その下に追加のボーナス質問があります):

  • システムコールを必要とせずに、一度に複数のコアで同時に複数のスレッドを実行できるアセンブラコードを書くことは可能ですか?
  • 本当に小さいスレッド (スレッドの合計実行時間のように小さい)、パフォーマンスの低下、またはまったく努力する価値がない場合、パフォーマンスは向上しますか?

私の推測では、マルチスレッド アセンブラ コードはシステム コールなしでは実現できないと思います。その場合でも、可能な限り効率的にスレッドを実装するための提案 (またはさらに良い: いくつかの実際のコード) はありますか?

4

7 に答える 7

28

簡単な答えはあなたができないということです。アセンブリコードを作成すると、1つの論理(つまりハードウェア)スレッドで順次(またはブランチを使用して)実行されます。コードの一部を別の論理スレッド(同じコア、同じCPUの別のコア、または別のCPU)で実行する場合は、OSに他のスレッドの命令ポインターを設定させる必要があります(CS:EIP)実行するコードをポイントします。これは、システムコールを使用して、OSに必要な処理を実行させることを意味します。

ユーザースレッドはすべて同じハードウェアスレッドで実行されるため、必要なスレッドサポートは提供されません。

編集: ParlanseにIraBaxterの答えを組み込む。プログラムで最初に各論理スレッドで実行されているスレッドがあることを確認すると、OSに依存せずに独自のスケジューラーを構築できます。いずれにせよ、あるスレッドから別のスレッドへのホッピングを処理するスケジューラーが必要です。スケジューラーの呼び出しの間に、マルチスレッドを処理するための特別なアセンブリ命令はありません。スケジューラ自体は特別なアセンブリに依存することはできませんが、各スレッドのスケジューラの部分間の規則に依存します。

いずれにせよ、OSを使用するかどうかに関係なく、クロススレッド実行を処理するにはスケジューラーに依存する必要があります。

于 2009-06-15T08:45:44.073 に答える
17

「医者、医者、私がこれをするときそれは痛い」。医者:「そんなことしないで」。

簡単に言うと、高価なOSタスク管理プリミティブを呼び出さなくてもマルチスレッドプログラミングを実行できます。スレッドスケジューリング操作のOSは無視してください。これは、独自のスレッドスケジューラを作成する必要があり、OSに制御を戻さないことを意味します。(そして、かなり賢いOSの人たちよりも、スレッドのオーバーヘッドについて何とか賢くなければなりません)。このアプローチを選択したのは、Windowsのプロセス/スレッド/ファイバーの呼び出しがすべて高すぎて、数百の命令の計算グレインをサポートできないためです。

当社のPARLANSEプログラミング言語は並列プログラミング言語です。http ://www.semdesigns.com/Products/Parlanse/index.htmlを参照してください。

PARLANSEはWindowsで実行され、抽象並列処理構造として並列「グレイン」を提供し、高度に調整された手書きスケジューラと、グレインのコンテキストを考慮してスケジューリングを最小化するPARLANSEコンパイラによって生成されたスケジューリングコードの組み合わせによって、そのようなグレインをスケジュールします。オーバーヘッド。たとえば、コンパイラは、スケジューリング(「待機」など)が必要になる可能性のある時点で、グレインのレジスタに情報が含まれていないことを確認します。したがって、スケジューラコードはPCとSPを保存するだけで済みます。実際、スケジューラコードがまったく制御できないことがよくあります。フォークされたグレインは、フォークするPCとSPを格納し、コンパイラーが事前に割り当てたスタックに切り替えて、グレインコードにジャンプします。グレインが完了すると、フォーカーが再起動します。

通常、グレインを同期するためのインターロックがあり、セマフォのカウントに相当するものを実装するネイティブのLOCKDEC命令を使用してコンパイラーによって実装されます。アプリケーションは論理的に何百万もの穀物をフォークすることができます。スケジューラーは、作業キューが十分に長い場合、親グレインがより多くの作業を生成することを制限するため、それ以上の作業は役に立ちません。スケジューラーはワークスティーリングを実装して、ワークが不足しているCPUが隣接するCPUワークキューからレディグレインを取得できるようにします。これは、最大32個のCPUを処理するために実装されています。しかし、x86ベンダーが今後数年間で実際にそれ以上の使用を圧倒する可能性があることを少し心配しています!

PARLANSEは成熟した言語です。1997年から使用しており、数百万ラインの並列アプリケーションを実装しています。

于 2009-06-16T04:48:33.080 に答える
8

ユーザーモードのスレッド化を実装します。

歴史的に、スレッドモデルはN:Mとして一般化されています。つまり、M個のカーネルモデルスレッドで実行されるN個のユーザーモードスレッドです。現代の使用法は1:1ですが、必ずしもそうとは限らず、必ずしもそうする必要はありません。

単一のカーネルスレッドで、任意の数のユーザーモードスレッドを自由に維持できます。すべてが同時に見えるように、十分な頻度でそれらを切り替えるのはあなたの責任です。もちろん、スレッドは先制ではなく協調的です。基本的に、yield()呼び出しを独自のコード全体に分散させて、定期的な切り替えが行われるようにします。

于 2009-04-03T17:38:11.607 に答える
5

パフォーマンスを向上させたい場合は、カーネル スレッドを活用する必要があります。複数の CPU コアで同時にコードを実行できるのは、カーネルだけです。プログラムが I/O バウンド (またはその他のブロッキング操作を実行) でない限り、ユーザーモードの協調マルチスレッド (ファイバーとも呼ばれます) を実行しても、パフォーマンスは向上しません。追加のコンテキスト スイッチを実行するだけですが、実際のスレッドが実行している 1 つの CPU はいずれにしても 100% で実行されます。

システムコールが高速になりました。最新の CPU はこの命令をサポートしており、古い命令sysenterよりも大幅に高速です。Linux が可能な限り最速の方法でシステム コールを行う方法については、この記事intも参照してください。

自動生成されたマルチスレッドで、パフォーマンスが向上するのに十分な時間スレッドが実行されていることを確認してください。短いコードを並列化しようとしないでください。スレッドの生成と結合に時間を浪費するだけです。また、メモリの影響にも注意してください (ただし、測定と予測はより困難です)。複数のスレッドが独立したデータ セットにアクセスしている場合は、キャッシュの一貫性の問題により、同じデータに繰り返しアクセスしている場合よりもはるかに高速に実行されます。

于 2009-04-10T14:49:37.293 に答える
3

まず、C でスレッドを使用する方法 (pthreads、POSIX theads) を学ぶ必要があります。GNU/Linux では、おそらく POSIX スレッドまたは GLib スレッドを使用したいと思うでしょう。次に、アセンブリ コードから C を呼び出すだけです。

ここにいくつかのポインタがあります:

于 2009-04-10T14:39:15.400 に答える
3

の代わりにsyscallorを使用すると、システム コールはそれほど遅くなくなりました。それでも、スレッドを作成または破棄するときのオーバーヘッドのみが発生します。それらが実行されると、システムコールはありません。ユーザー モード スレッドは 1 つのコアでしか実行されないため、あまり役に立ちません。sysenterint

于 2009-04-10T14:44:59.973 に答える
3

かなり遅くなりましたが、私自身、この種のトピックに興味がありました。実際、並列化/パフォーマンスを除いて、カーネルが介入することを特に必要とするスレッドについて特別なことは何もありません。

必須の BLUF :

Q1: いいえ。さまざまな CPU コア/ハイパースレッドにわたって複数のカーネル スレッドを作成するには、少なくとも最初のシステム コールが必要です。

Q2: 場合によります。小さな操作を実行するスレッドを作成/破棄すると、リソースが浪費されます (スレッドの作成プロセスは、トレッドが終了するまでの時間を大幅に超えます)。N 個のスレッド (N はシステム上のコア/ハイパースレッドの最大数) を作成し、それらを再タスクする場合、実装によっては答えが「はい」になる可能性があります。

Q3: 操作を順序付ける正確な方法を前もって知っていれば、操作を最適化できます。具体的には、ROP チェーン (または転送呼び出しチェーンですが、実際には実装がより複雑になる可能性があります) に相当するものを作成できます。この ROP チェーン (スレッドによって実行される) は、「ret」命令を (独自のスタックに対して) 継続的に実行し、そのスタックは継続的に先頭に追加されます (または、先頭にロールオーバーする場合は追加されます)。そのような (奇妙な!) モデルでは、スケジューラーは各スレッドの「ROP チェーンの終わり」へのポインターを保持し、そこに新しい値を書き込みます。これにより、コードは関数コードを実行するメモリーを循環し、最終的には ret 命令になります。繰り返しますが、これは奇妙なモデルですが、それでも興味深いものです。

私の2セント相当のコンテンツに。

私は最近、さまざまなスタック領域 (mmap を介して作成) を管理し、「スレッド」の制御/個別化情報を格納するための専用領域を維持することにより、純粋なアセンブリでスレッドとして効果的に動作するものを作成しました。このように設計したわけではありませんが、mmap を介して 1 つの大きなメモリ ブロックを作成し、それを各スレッドの「プライベート」領域に分割することは可能です。したがって、必要なシステムコールは 1 つだけです (ガードページ間のガードページはスマートですが、追加のシステムコールが必要になります)。

この実装では、プロセスが生成されたときに作成されたベース カーネル スレッドのみが使用され、プログラムの実行全体を通じて 1 つのユーザーモード スレッドのみが存在します。プログラムは自身の状態を更新し、内部制御構造を介してスケジュールを設定します。I/O などは、(複雑さを軽減するために) 可能な場合はブロック オプションを介して処理されますが、これは厳密には必須ではありません。もちろん、ミューテックスとセマフォを利用しました。

このシステムを (完全にユーザー空間で、必要に応じて非ルート アクセス経由でも) 実装するには、次のものが必要でした。

スレッドの要約: スタック操作用のスタック (自明で自明) 実行する一連の命令 (これも自明) 個々のレジスタの内容を保持するメモリの小さなブロック

スケジューラの概要: スケジューラで指定された順序付きリスト (通常は優先度) 内の一連のスレッド (プロセスが実際に実行されることはなく、スレッドのみが実行されることに注意してください) のマネージャー。

スレッド コンテキスト スイッチャー: コードのさまざまな部分に挿入されるマクロ (通常、これらは負荷の高い関数の最後に配置します) は、スレッドの状態を保存し、別のスレッドの状態をロードする「スレッド イールド」とほぼ同等です。

そのため、ルート以外のプロセスでユーザーモードのスレッドのような構成を作成することは、(完全にアセンブリで、最初の mmap と mprotect 以外のシステム コールなしで) 実際に可能です。

この回答を追加したのは、x86アセンブリについて具体的に言及しており、この回答は、システムコールを最小限に抑え、システム側のスレッドも最小限に抑えるという目標(マルチコア機能を除く)を達成するx86アセンブリで完全に記述された自己完結型プログラムを介して完全に導出されたためです。オーバーヘッド。

于 2016-10-14T01:47:37.343 に答える