トラック
/
x86-64 Assembly
x86-64 Assembly
/
演習
/
ミキシングコンソール
ミキシングコンソール

ミキシングコンソール

学習演習

はじめに

SIMD:整数

前の概念では、パックされた浮動小数点値、つまり複数の数値を1つのxmmレジスタに保持し、レーンごとに並列で操作する方法を紹介しました。 同じxmmレジスタは、パックされた_整数_を保持することもできます。

構文

SIMD浮動小数点モデルのほとんどは、パックされた整数にもそのまま当てはまります。

  • 128ビットのレジスタはレーンに分割されます
  • 命令は、同じ位置にあるレーンに対して並列に作用します
  • メモリオペランドは、同じ16バイトのアラインメント規則に従います

ただし、構文は少し異なります。

  1. 命令が_パックされた_データを操作することを示す_接頭辞_p。
  2. 実行される演算。非SIMD(_スカラー_とも呼ばれます)の対応する演算と同じ名前です(例:add、mulなど)。
  3. 各レーンのサイズを示す接尾辞。

パックされた整数には4つのレーンサイズがあり、それぞれに独自の接尾辞があります。

レーン幅 バイト数 128ビット内のレーン数 接尾辞
バイト 1 16 b
ワード 2 8 w
ダブルワード 4 4 d
クアッドワード 8 2 q

たとえば、次のようになります。

命令 意味
paddb パックされたadd、8ビットレーン(16レーン)
paddw パックされたadd、16ビットレーン(8レーン)
paddd パックされたadd、32ビットレーン(4レーン)
paddq パックされたadd、64ビットレーン(2レーン)

一部の命令は、あるサイズのレーンを入力として受け取り、別のサイズを出力とします。 それらは同じ一般的な規則に従いますが、サイズ接尾辞が_2つ_付きます。 最初の接尾辞は入力レーンのサイズを示し、2番目は出力レーンのサイズを示します。

命令 意味
pmovsxwd パックされたmovsx、16ビットレーンから32ビットレーンへ
pmuldq パックされたmul、32ビットレーンから64ビットレーンへ

メモリの移動

2つの整数名の命令は、xmmレジスタとメモリの間で128ビットをコピーします。

命令 説明
movdqa _アラインメントされた_場所との間でパックされた整数をコピーします
movdqu _アラインメントされていない_場所との間でパックされた整数をコピーします

これらはmovapsとmovupsと同様に動作します。movdqaはアラインメントされていないアドレスでフォールトしますが、movdquはどのアドレスも受け付けます。 4つとも、128ビットを解釈せずにコピーします。 整数名のペアは、要件ではなく慣例として整数データと一緒に使われます。

Note

ここでdqはdouble-qword、つまり128ビット(16バイト)を表します。

加算と減算

加算と減算は命名規則に従います。

paddb xmm0, xmm1 ; 16 lanes: each  8-bit, xmm0 += xmm1
paddw xmm2, xmm3 ;  8 lanes: each 16-bit, xmm2 += xmm3

psubd xmm4, xmm5 ;  4 lanes: each 32-bit, xmm4 -= xmm5
psubq xmm6, xmm7 ;  2 lanes: each 64-bit, xmm6 -= xmm7

符号付きと符号なしの別形式はありません。 2の補数では、レーンを符号付きとして読むか符号なしとして読むかに関係なく、加算と減算は同じビットを生成するため、1つの命令で両方をまかなえます。 解釈は、スカラーのaddとsubの場合とまったく同じで、使う側が決めます。

これらの命令は、スカラーの対応命令と同様に、オーバーフロー時にラップアラウンドします。 8ビットレーンは値を256を法として保持するため、paddbで200 + 100を計算すると300 - 256 = 44となり、収まらないビットは切り捨てられます。

これらの余分なビットは次のレーンに繰り越されないことに注意してください。 各レーンは、同じレジスタを共有していても、他のレーンとは別々に操作されます。

飽和加算と減算

整数SIMDには、浮動小数点SIMDにはない演算が加わります。飽和加算と減算で、ラップアラウンドする代わりに_クランプ_します。 レーンの範囲を超える結果は、そのレーンが保持できる最大値になり、範囲を下回る結果は最小値になります。

飽和形式では、サイズ接尾辞の前にs(符号付き)またはus(符号なし)を挿入します。

命令 意味
paddsb add、飽和、符号付き8ビットレーン
paddusb add、飽和、符号なし8ビットレーン
psubsw sub、飽和、符号付き16ビットレーン
psubusw sub、飽和、符号なし16ビットレーン

クランプ範囲は、対応するサイズと符号属性を持つ整数の表現可能な全範囲です。 バイトの場合:

  • 符号なしバイトは[0, 255]にクランプされます。paddusbで200 + 100を計算すると255になり、psubusbで5 - 10を計算すると0になります。
  • 符号付きバイトは[-128, 127]にクランプされます。paddsbで100 + 50を計算すると127になります。

飽和は、ピクセルのチャンネルや音声サンプルのように、レーンが有限の量を保持する場合に重要です。 ラップアラウンドでは明るすぎるピクセルが暗くなってしまいますが、クランプなら最大の明るさを保てます。これが望ましい結果です。

Note

飽和加算と減算はバイトとワードのレーンにのみ存在し、ダブルワードやクアッドワードにはありません。

乗算

2つのNビット値を乗算すると2Nビットの積が生成されることがありますが、宛先レーンの幅はNビットしかありません。 SIMD演算では、積のどちらの半分を保持するか、つまり下位Nビットか上位Nビットかを指定することでこれを解決します。

16ビットレーンの場合、3つの命令を使います。

命令 意味
pmullw mul、16ビットレーン、各積の下位16ビットを保持
pmulhw mul、16ビットレーン、上位16ビットを保持、符号付きオペランド
pmulhuw mul、16ビットレーン、上位16ビットを保持、符号なしオペランド

積の下位16ビットは、オペランドを符号付きとして読んでも符号なしとして読んでも同じなので、pmullwは1つだけです。

ただし、上位16ビットは結果の符号属性によって異なります。 そのため、上位半分の乗算には符号付き乗算と符号なし乗算の別形式があります。

  1. pmulhw:符号付き乗算用。
  2. pmulhuw:余分なuが付き、符号なし乗算用。

構文に注意してください。

  1. パックされた整数を表すp。
  2. 実行される演算mul。
  3. 結果の上位("high")ビットが選択されることを示すh。
  4. 結果を符号なしとして解釈する場合の任意のu(つまり、符号拡張されません)。
  5. 最後に、ワード(16ビット)演算であることを示すサイズ接尾辞w。

ワードのパック乗算は上記の規則に正確に従います。 ダブルワード(32ビット)乗算もこの規則に従いますが、下位半分を選択する変種しかありません。pmulldです。

上位ビットを選択するダブルワード乗算の変種はありません。 ただし、乗算を_拡張_し、_偶数インデックス_のレーン(つまり位置0と2のレーン)の完全な64ビット積を格納する変種があります。

命令 意味
pmuludq 偶数インデックスの32ビット要素を符号なしで乗算し、2つの完全な64ビット積にする
pmuldq 偶数インデックスの32ビット要素を符号付きで乗算し、2つの完全な64ビット積にする

構文ではdqが使われ、符号なし乗算の場合はその前にuが付くことがあります。 これは、命令がdwordレーンを入力として受け取り、qwordレーンを出力するためです。

除算

パック整数の除算はありません。 必要なコードは、値を浮動小数点に変換して除算し、元に戻します。

整数の拡張

movsxとmovzxにはパック版があります。 これらは、出力とは異なるレーンサイズの入力を取る命令について説明したのと同じ構文に従います。

pmovsxwd xmm0, xmm1   ; 4 words -> 4 dwords, sign-extended
pmovzxbw xmm0, xmm1   ; 8 bytes -> 8 words, zero-extended

レーン数は、より大きい方(出力)の幅によって決まることに注意してください。 命令は、ソースの下位部分からその数のレーンを読み取ります。 これは、単精度と倍精度の浮動小数点間のパックcvt命令ですでに見たのと同じ意味論です。

整数と浮動小数点の変換

32ビット符号付き整数と浮動小数点レーンの間で変換する命令があります。 これらはcvtおよびcvtt命令の通常の構文に従いますが、siの代わりに、パックされた整数はdqで表されます。

cvtdq2ps xmm0, xmm1 ; convert 32-bit signed integers in xmm1 to 32-bit floats in xmm0
cvtdq2pd xmm2, xmm3 ; convert 32-bit signed integers in xmm3 to 64-bit floats in xmm2
cvtps2dq xmm4, xmm5 ; convert 32-bit floats in xmm5 to 32-bit signed integers in xmm4
Caution

パックされた整数を表すためにpiを使う命令は、レガシーなmmxレジスタに書き込み、x86-64では事実上非推奨です。 xmmレジスタを使うdq形式を優先してください。

浮動小数点数と整数の間のスカラー変換について述べた同じ注意点がここでも当てはまります。 浮動小数点数はMXCSRと呼ばれる特別なレジスタに従って丸められ、そのモードは関数に入った時点で仮定できません。 結果を常に切り捨てるcvtt変種(余分なtが付きます)があります。

変換前にパックされた浮動小数点値を既知の状態にするために、roundを使うこともできます。 丸め制御値はスカラーのroundと同じで、構文もパックされた浮動小数点値の場合と同じです。

roundps xmm0, xmm1, 1 ; xmm0 = floor(xmm1), packed 32-bit floats
roundpd xmm2, xmm3, 2 ; xmm2 = ceil(xmm3), packed 64-bit floats

説明

録音スタジオにある小さなミキシングコンソールのデジタルコアを書いています。

音はサンプルのストリームとして届きます。 各サンプルは、ある瞬間のスピーカーのコーンの位置を表す数値です。 コンソールはサンプルのブロックをまとめて処理し、それぞれに同じ演算を適用します。

5つのタスクがあり、それぞれがメモリ上に保持されたサンプルのブロックを処理します。 オペランドはメモリアドレスを通じて受け取り、答えは結果アドレスを通じて書き込みます。

Note

この演習の計算は、スカラー演算ではなくSIMD命令を使って行う必要があります。

1. 2つのトラックをミックスする

スタジオでは、楽器を別々のトラックに録音することがあります。 同じスピーカーから再生するには、2つのトラックを1つに足し合わせます。

mix_tracks関数を実装します。この関数は、2つのトラックをサンプルごとに足し合わせます。 各トラックは、8個の符号付き16ビットサンプルのブロックです。 符号付き16ビットの範囲を超える和は、ラップせずに飽和させる必要があります。

この関数は、次の順番で引数を取ります。

  • result:ミックス後の8個のサンプルを書き込むバッファーの、16バイト境界に整列されたメモリアドレス。
  • track_a:1つ目のトラックの16バイト境界に整列されたメモリアドレスで、8個の符号付き16ビットサンプルを持ちます。
  • track_b:2つ目のトラックの16バイト境界に整列されたメモリアドレスで、8個の符号付き16ビットサンプルを持ちます。
track_a = {10000, 30000, 20000, -30000, -20000, -100,  100, 0}
track_b = { 5000,  5000, 20000,  -5000, -20000,  100, -100, 0}
result  = {15000, 32767, 32767, -32768, -32768,    0,    0, 0}

この関数に戻り値はありません。

2. ブリードを取り除く

2つのマイクで同時に録音すると、それぞれがもう一方の音源のわずかなコピーを拾ってしまいます。 この不要なコピーは_ブリード_と呼ばれます。 このブリードを測定したら、トラックをきれいにするためにそれを差し引く必要があります。

remove_bleed関数を実装します。この関数は、トラックのブロックからブリードのブロックをサンプルごとに差し引きます。 各ブロックは8個の符号付き16ビットサンプルで、引き算は飽和させる必要があります。

この関数は、次の順番で引数を取ります。

  • result:きれいにした8個のサンプルを書き込むメモリアドレス(整列は保証されません)。
  • track:録音したトラックの16バイト境界に整列されたメモリアドレスで、8個の符号付き16ビットサンプルを持ちます。
  • bleed:取り除くブリードのメモリアドレスで、8個の符号付き16ビットサンプルを持ちます(整列は保証されません)。
track  = {10000, -30000, 5000, 32767, 0,  200, -200, 15000}
bleed  = { 2000,   5000, 5000,    -5, 0,  300,  300,  5000}
result = { 8000, -32768,    0, 32767, 0, -100, -500, 10000}

この関数に戻り値はありません。

3. メーターをまとめる

各チャンネルの上にはレベルメーターがあり、その明るさでチャンネルの音の大きさを示す小さなバーです。 フロントパネルにはマスターメーターが1つあり、これは2つのチャンネルを合わせたのと同じくらい明るく光る必要があります。 これを動かすために、2つのメーターブロックを足し合わせます。

明るいメーターは、暗さに戻るようにラップするのではなく最大の明るさにとどまるため、和は飽和する点に注意してください。

combine_meters関数を実装します。この関数は、2つのメーターブロックをサンプルごとに足し合わせます。 この関数は、次の順番で引数を取ります。

  • result:足し合わせた16個の値を書き込む、16バイト境界に整列されたメモリアドレス。
  • meter_a:1つ目のチャンネルのメーターブロックの16バイト境界に整列されたメモリアドレスで、16個のパックされた符号なし8ビット値を持ちます。
  • meter_b:2つ目のチャンネルのメーターブロックの16バイト境界に整列されたメモリアドレスで、16個のパックされた符号なし8ビット値を持ちます。

各ブロックは16個の符号なし8ビット値です。 これらは足し合わせ、符号なしの範囲に飽和させます。

meter_a = {200, 100,  50, 255, 0, 128, 10, 240, 1, 2, 3, 4, 5, 6, 7, 8}
meter_b = {100, 100, 200,   1, 0, 128, 10,  20, 1, 2, 3, 4, 5, 6, 7, 8}
result  = {255, 200, 250, 255, 0, 255, 20, 255, 2, 4, 6, 8, 10, 12, 14, 16}

この関数に戻り値はありません。

4. フェードをかける

曲は急に止まるのではなく、最後にフェードアウトするようにします。 サンプルごとに1つずつ、少しずつ小さくなっていく_ゲイン係数_のブロックがあります。

各係数は、65536を分母とする分数で表されます。 たとえば、あるサンプルの値が16384なら、実際には16384 / 65536、つまり1 / 4を表します。 使える最大の値である32767は、ちょうど2分の1より少し小さい値です。

65536は2^16なので、サンプルにその係数を掛けて積の_上位半分_を取ると、sample * gain / 65536になります。

apply_fade関数を実装します。この関数は、各サンプルをその係数でスケーリングします。 この関数は、次の順番で引数を取ります。

  • result:スケーリングしたすべてのサンプルを書き込む、16バイト境界に整列されたメモリアドレス。
  • track:トラックの16バイト境界に整列されたメモリアドレスで、n個の符号付き16ビットサンプルを持ちます。
  • gains:ゲイン係数の16バイト境界に整列されたメモリアドレスで、n個の符号付き16ビット値を持ち、それぞれが0から32767の範囲にあります。
  • n:サンプルの個数で、常に0以外かつ8の倍数であり、64ビットの符号なし整数です。

トラックはn個の符号付き16ビットサンプルを保持する配列で、nは決して0ではなく常に8の倍数である点に注意してください。 各サンプルについて、スケーリングした結果は、符号付きの積sample * gainの_上位_16ビットです。

track  = {20000, 20000, 20000, 20000, -20000, -20000, -20000, -20000}
gains  = {32767, 24576, 16384,  8192,  32767,  24576,  16384,   8192}
result = { 9999,  7500,  5000,  2500, -10000,  -7500,  -5000,  -2500}
Note

上位半分を取ると、負の無限大方向に丸められます。 そのため、20000 * 32767は9999にスケーリングされますが、-20000 * 32767は-9999ではなく-10000にスケーリングされます。

この関数に戻り値はありません。

5. トラックを減衰させる

ドラマーは音が大きすぎる状態で録音されていました。 トラック全体を小さくするために、すべてのサンプルを定数で割ります。

attenuate_track関数を実装します。 この関数は、次の順番で引数を取ります。

  • result:結果を32ビットの符号付き整数として書き込む、16バイト境界に整列されたメモリアドレス。
  • samples:サンプルの16バイト境界に整列されたメモリアドレスで、n個の符号付き16ビット値を持ちます。
  • divisor:除数を4つ複製したものの16バイト境界に整列されたメモリアドレスで、32ビット整数です。
  • nはサンプルの個数で、常に0以外かつ4の倍数であり、64ビットの符号なし整数です。

samplesはn個の符号付き16ビットサンプルを保持する配列で、nは決して0ではなく常に4の倍数である点に注意してください。

除数は、同じ32ビット整数を4つ複製した形で渡され、4つのレーンすべてを一度に埋められるようあらかじめ複製されています。

整数SIMDにはパック除算がないため、処理はパック浮動小数点で行い、その後、resultに格納するためにパックされた32ビット整数に変換し直します。 ゼロ方向に切り捨てて変換し直すので、商が2.3なら2に、-2.3なら-2になります。

samples = {300, 150, 90, -120, 100, 50, -50, 7}
divisor = {3, 3, 3, 3}
result  = {100,  50, 30,  -40,  33, 16, -16, 2}

この関数に戻り値はありません。

GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
x86-64 Assembly Exercism

ミキシングコンソールを始める準備はできましたか?

Exercismに登録すれば、22個のコンセプト130個の演習、そして本物の人間によるメンタリングとともに、x86-64 Assemblyを学んでマスターできます。すべて無料です。