私は 8086 Assembly を知っていて、今はMIPS Assembly Language ProgrammingとSee MIPS Runという本を読んで MIPS Assembly を学んでいますが、Assembly のコーディング標準/ベスト プラクティスについて考えるのをやめませんでした。私は毎日より良い開発者になりたいと思っています。そして、自分自身を改善するためにこれを知りたい. アセンブリ コーディングの標準とベスト プラクティスについて詳しく知るにはどうすればよいですか?
2 に答える
ベストプラクティスは、作業する社会に応じた社会現象であるため、最善の答えは、対話する予定の環境から既存のMIPSasmコードを読み取ることです。
私自身の世界から思い浮かぶ例は、Linuxカーネルのアセンブラーセクション、GCCのMIPSスタートアップコード、またはglibcのMIPSポートのアセンブラーフラグメントです。
主に他のプロジェクトとやり取りする場合は、そのコミュニティのコーディング慣行を吸収して模倣するのが最善です。
優れた asm スタイルは、ISA (および同じ CPU の asm のさまざまな方言) 全体でかなり普遍的です。コンパイラ出力 (gcc/clang など) は通常、以下で説明するすべてのことを行うため、良いガイドラインです。(そして、C コンパイラの出力は、多くの場合、小さな関数を最適化するための適切な出発点です。)
通常、命令は、ラベルやアセンブラ ディレクティブよりも 1 レベル深くインデントします。
オペランドを一貫性のある列にインデントします (ニーモニックの長さを変えてもコードが不規則にならないようにし、ブロックを下方向にスキャンして、すべての命令の宛先レジスタを最初のオペランドとして確認するのは簡単です) 1 .
命令行のコメントは、視覚的なノイズを避けるために、オペランドを十分に超えて右側の一貫した列にインデントします。
関連する命令のブロックをグループ化し、それらを区切る空白行を使用します。(または、命令をスケジュールすることによってインオーダー CPU 用に最適化している場合、実際にはこれを行うことができず、コメントを使用して、各命令が問題のどの部分に取り組んでいるかを追跡する必要があります。コメントは役に立ちます)
脚注 1:最初のオペランドが実際にソースである場合
など、MIPS ストア命令を除きます。sw $t0, 1234($t1)
彼らは、asm ソースがロードとストアの両方に同じオペランド順序を使用するようにすることを選択しました。これは、おそらくマシン コードの両方が I タイプの命令であるためです。mov eax, [rdi]
ただし、これは RISC ロード/ストア アーキテクチャの asm の典型であるため、 がロードでがストアである CISC から来ることに慣れる必要がありますmov [rdi], eax
。そしてadd [rdi], eax
両方です。
例:atoi
分岐遅延スロットを持つ実際の MIPS の符号なし整数の関数。ただし、MIPS I ではなく、ロード遅延スロットはありません。とにかくロード使用のストールを回避しようとしましたが。( C版のゴッドボルト)
# unsigned decimal ASCII string to integer
# inputs: char* in $a0 - ASCII string that ends with a non-digit character
# outputs: integer in $v0
# clobbers: $t0, $t1
atoi:
# peel the first iteration to avoid a 0 * 10 multiply
lbu $v0, 0($a0)
addiu $v0, $v0, -'0' # digit = *p - '0'
sltu $t0, $v0, 10
bnez $t0, .Lloop_entry # if unsigned (! digit<10)
nop # doing work for the next iteration here hurts ILP for in-order CPUs
#addu $t2, $v0, $v0 # total * 2 (branch delay slot)
# invalid non-digit input
jr $ra # return 0
move $v0, $zero
.Lloop: # do {
addu $v0, $v0, $v0 # total *= 2
addu $t0, $t0, $t1 # total*8 + digit
addu $v0, $v0, $t0 # total*10 + digit = total*2 + (total*8 + digit)
.Lloop_entry:
lbu $t0, 1($a0)
addui $a0, $a0, 1 # t0 = *(p++ + 1)
addiu $t0, $t0, -'0' # t0 = digit
sltu $t1, $t0, 10
bnez $t1, .Lloop # while(digit<10);
sll $t1, $v0, 3
jr $ra
nop
これは、特定の MIPS 実装にとっておそらく最適ではありません。順序付けされたスーパースカラーは、ロードと分岐の間により多くのシフト/追加を配置することでおそらく利益を得るでしょうが、それは最後の反復でより冗長な作業が行われることを意味します. r10k のような OoO exec にはおそらく適しています。最新の MIPS32r6 はlsa
、gcc が で行うように、左シフト累積に-march=mips32r6
使用し、分岐命令の分岐遅延なしバージョンを使用します。
ただし、これは初期のスカラー MIPS ではかなり良いかもしれません。ポインターのインクリメントは、ロード後にスロットを埋め、ループ内での失速を回避します。(即時のオフセット 1 は、皮をむいた最初の反復でインクリメントを回避したためです)。
メインループ内の後の.Lloop_entry
次の反復のためにさらに多くのものを計算したい場合は、スタートアップブランチの遅延スロットを埋めることができます。しかし、それには への依存が必要になり、スーパースカラーのインオーダー CPU の ILP に悪影響を及ぼします。(現在、 top to命令は並行して実行でき、次に新しい合計を生成するには、 と並行して実行できます。)addu $v0, $v0, $t0
$v0
addu
addu
lbu
スカラーの順番 (MIPS I / MIPS II など) や順不同の CPU では問題ありません。
(条件付き分岐が前のALU命令から入力を読み取るときに初期のMIPSがストールする必要があるかどうかはわかりませんが、分岐の決定はEXの1サイクル前のIDステージにあります。しかし、おそらくMIPSが文字通り持っていなかったからではありませんRAW ハザードのパイプライン インターロック; そのため、ロード遅延スロットがありました。)