Main Content

カスタム tall 配列アルゴリズムの開発

tall 配列は、従来の MATLAB® 構文を使用して大規模なデータ セットを扱うための強力で直感的な方法です。ただし、tall 配列は、それぞれが個別にメモリに収まるデータ ブロックを操作するため、ほとんどの関数の従来のアルゴリズムは、tall 配列をサポートするために並列化アプローチを使用するよう更新しなければなりません。このトピックでは、tall 配列を操作するための独自の並列化アルゴリズムの開発方法を説明します。

カスタム関数を tall 配列に適用するために現在使用できるアプローチは、次のとおりです。

どの操作を選択するかに関わりなく、すべてのアプローチに適用されるオプション、パフォーマンス上の考慮事項、および一般的な問題があります。

カスタム アルゴリズムを実装する理由

ほとんどの一般的な数学関数と MATLAB 演算では、既に tall 配列がサポートされています。機能が既にサポートされている場合は、独自のアルゴリズムを作成する必要はないかもしれません。

以下に、tall 配列用のカスタム アルゴリズムを実装することが望ましい理由をいくつか示します。

  • 現在サポートされていない関数の実装 — 特定の関数が現在 tall 配列をサポートしていない場合は、ここで概説する API を使用して、tall 配列をサポートするその関数のバージョンを作成できます。

  • 既存のコードの活用 — インメモリ データに対していくつかの操作を実行する既存のコードがある場合は、わずかな変更だけで、tall 配列を扱うように互換性をもたせることができます。このアプローチでは、tall 配列をサポートする MATLAB 言語のサブセットに合わせてコードを変換する必要がありません。

  • パフォーマンスの向上 — たとえば、MATLAB 関数を C++ MEX 関数として書き換えることができ、ここで概説する API を使用して、データを扱う MEX 関数を呼び出すことができます。

  • 優先外部ライブラリの使用 — 組織内での互換性のために、特定の外部ライブラリを特定の計算に使用するよう求められる場合があります。ここで概説する API を使用して、これらの外部ライブラリの関数を再実装することができます。

サポートされている API

サポートされている API は高度な使用を目的としており、広範な入力チェックは含まれていません。実装した補助関数がすべての要件を満たし、期待どおりの計算を行えることをテストするには、ある程度の時間がかかることが予想されます。tall 配列アルゴリズムを作成するために現在サポートされている API を次に示します。

名前説明
matlab.tall.transform指定された関数を 1 つ以上の tall 配列の各ブロックに適用します。
matlab.tall.reduce指定された関数を 1 つ以上の tall 配列の各ブロックに適用します。その後、この関数の出力を 2 つ目のリダクション関数に渡します。
matlab.tall.movingWindow移動ウィンドウ関数をデータ ブロックに適用します。
matlab.tall.blockMovingWindow

移動ウィンドウ関数およびブロック削減をパディングされたデータ ブロックに適用します。

背景: tall 配列ブロック

データストアから tall 配列を作成すると、基となるデータストアによって、計算中のデータの移動が容易になります。データは "ブロック" と呼ばれる別々の要素に分かれて移動します。各ブロックはメモリに収容可能な連続する行のセットです。たとえば、2 次元配列 (table など) の 1 つのブロックは X(n:m,:) です。各ブロックのサイズは、データストアの ReadSize プロパティの値に基づいていますが、ブロックが必ずしも正確にそのサイズになるわけではありません。tall 配列アルゴリズムを開発する場合、tall 配列はそうした多数のブロックを垂直方向に連結したものと見なされます。

Image of an array divided into vertical blocks.

配列のブロックは、使用可能なメモリに基づいて実行時に選択されるため、動的となる場合があります。そのため、ブロックは実行ごとに "厳密には" 同じサイズにならないことがあります。コンピューターで、使用可能なメモリに影響を与える変更がある場合、それがブロックのサイズに影響を与える可能性があります。

このページでは、2 次元の観点から "ブロック" と "行" に言及していますが、この概念は N 次元の tall 配列に拡張されます。ブロック サイズは最初の次元でのみ制約されるため、ブロックには、たとえば X(n:m,:,:,...) のように他の次元の要素がすべて含まれます。また、N 次元配列には、行ではなく X(p,:,:,...) のような "スライス" が含まれています。

単一ステップの変換操作

関数 matlab.tall.transform は、tall 配列の各ブロックに 1 つの関数を適用するため、データの変換、フィルター処理、または削減をブロック単位で適用するために使用できます。たとえば、特定の値をもつ行を削除し、データのセンタリングやスケーリングを行い、あるいは特定の条件を検出して特定データを変換することができます。以下の図は、matlab.tall.transform によって配列のブロックを操作したときの動作を示しています。

操作

説明

Illustration of transformation operation, where the number of rows in each block remains constant.

変換 — 各ブロックの行数は同じままですが、値が変わります。

  • A = matlab.tall.transform(@sin, tX) は各ブロックの要素の正弦を計算する。

  • A = matlab.tall.transform(@(X) X.^2, tX) は各ブロックの要素を二乗する。

Illustration of filtering operation, where the number of rows in each block are reduced.

フィルター処理 — 各ブロックの行数が減少します。そのため、新しい配列内のブロックには前に他のブロックにあった行が含まれる可能性があります。

  • A = matlab.tall.transform(@(X) topkrows(X,5), tX) は各ブロックから上位 5 行のみを抽出し、他の行を除外する。

  • A = matlab.tall.transform(@sum, tX) は各ブロックの要素の合計を計算し、そのため各ブロックはスカラーに縮小される。A の要素数はブロックの数と同じです。

変換構文

単一ステップの変換を適用する汎用構文は、次のとおりです。

[tA, tB, tC, ...] = matlab.tall.transform(fcn, tX, tY, tZ, ...)

fcn の機能要件

fcn の一般的な関数シグネチャは、次のとおりです。

[a, b, c, ...] = fcn(x, y, z, ...)
fcn は以下の要件を満たさなければなりません。

  1. 入力引数 — 入力 [x, y, z, ...] は、メモリに収まるデータのブロックです。ブロックは、tall 配列の入力 [tX, tY, tZ, ...] それぞれからデータを抽出することによって生成されます。入力 [x, y, z, ...] は次の特性を満たします。

    • [x, y, z, ...] はすべて、任意の可能な拡張後の最初の次元が同じサイズをもつ。

    • [x, y, z, ...] のデータ ブロックは、tall 配列の tall 次元が 1 ではないと仮定した場合、tall 次元の同じインデックスから得られる。たとえば、tXtY の tall 次元が 1 ではない場合、ブロックの最初のセットは x = tX(1:20000,:) および y = tY(1:20000,:) になる可能性があります。

    • [tX, tY, tZ, ...] のいずれかの最初の次元がサイズ 1 である場合、対応するブロック [x, y, z, ...] は、その tall 配列内のすべてのデータで構成される。

  2. 出力引数 — 出力 [a, b, c, ...] はメモリに収まるブロックで、それぞれの出力 [tA, tB, tC, ...] に送信されます。出力 [a, b, c, ...] は以下の特性を満たします。

    • [a, b, c, ...] はすべて、最初の次元が同じサイズでなければならない。

    • [a, b, c, ...] はすべて、fcn の以前の呼び出しの結果とそれぞれ垂直方向に連結される。

    • [a, b, c, ...] はすべて、それぞれの出力先で、出力配列の最初の次元にある同じインデックスに送信される。

  3. 関数ルールfcn は関数ルールを満たさなければなりません。

    • F([inputs1; inputs2]) == [F(inputs1); F(inputs2)]:入力の連結に関数を適用することは、入力に関数を個別に適用してから結果を連結する場合と同じでなければならない。

  4. 空の入力fcn が高さ 0 の入力を処理できることを確認します。空の入力は、ファイルが空の場合や、データに対して数多くのフィルター処理を行った場合に発生することがあります。

2 ステップのリダクション演算

matlab.tall.reduce は 2 つの関数を tall 配列に適用し、最初のステップの結果を最後のリダクション ステップに入力として送ります。リダクション関数は、メモリに収まる単一の最終ブロックが得られるまで、中間結果に繰り返し適用されます。MapReduce のパラダイムでは、この処理は "単一キー" の MapReduce 演算に似ています。そこでは、中間結果がすべて同じキーをもち、リダクション ステップで結合されます。

最初のステップは matlab.tall.transform に似ており、その要件は同じです。ただし、リダクション ステップでは、中間結果がメモリに収まる単一のブロックへと常に縮小されます。以下の図は、matlab.tall.reduce によって配列のブロックを操作したときの動作を示しています。

操作

説明

Illustration of compound transformation plus reduction operations, where rows are transformed before being reduced to one block.

変換 + リダクション — 各ブロックの行数は最初のステップの後も同じままで、その後、中間結果が 1 つのブロックに縮小されます。

  • A = matlab.tall.reduce(@sin,@max,tX) は各ブロックの値の正弦を計算したうえで、リダクション ステップの間に全体の最大値を見つける。

  • A = matlab.tall.reduce(@(X) X.^2, @mean, tX) は各ブロックの要素を二乗したうえで、リダクション ステップで全体の平均を計算する。

Illustration of compound filtering plus reduction operations, where rows are filtered down before being reduced to one block.

フィルター処理 + リダクション — 最初のステップで、各ブロックの行数が減らされます。その後、中間結果が 1 ブロックへと縮小されます。

  • A = matlab.tall.reduce(@sum, @sum, tX) は各ブロックの要素の合計を計算したうえで、リダクション ステップで要素全体の合計数を求める。

  • A = matlab.tall.reduce(@(X) X(X>0), @mean, tX) は負の値をすべて除外したうえで、残りの値全体の平均を計算する。

reduce の構文

2 ステップのリダクションを適用する汎用構文を次に示します。

[rA, rB, rC, ...] = matlab.tall.reduce(fcn, reducefcn, tX, tY, tZ, ...)

fcn の関数シグネチャは、次のとおりです。

[a, b, c, ...] = fcn(x, y, z, ...)

reducefcn の関数シグネチャは、次のとおりです。

[rA, rB, rC, ...] = reducefcn(a, b, c, ...)

つまり、入力の tall 配列 [tX, tY, tZ, ...] は、fcn への入力であるブロック [x, y, z, ...] に分割されます。その後、fcnreducefcn への入力である出力 [a, b, c, ...] を返します。最後に、reducefcn は、matlab.tall.reduce によって返される最終的な結果 [rA, rB, rC] を返します。

reducefcn の機能要件

fcn の要件は、fcn の機能要件で概説したものと同じです。ただし、reducefcn の要件は異なります。

reducefcn の一般的な関数シグネチャは、次のとおりです。

[rA, rB, rC, ...] = reducefcn(a, b, c, ...)
reducefcn は以下の要件を満たさなければなりません。

  1. 入力引数 — 入力 [a, b, c, ...] はメモリに収まるブロックです。データのブロックは、fcn によって返される出力か、さらなる縮小のために再度処理される、ある程度縮小された reducefcn からの出力のいずれかです。入力 [a, b, c, ...] は次の特性を満たします。

    • 入力 [a, b, c, ...] は、最初の次元が同じサイズとなる。

    • 最初の次元の特定インデックスについて、データ [a, b, c, ...] のブロックの各行は入力から派生するか、または reducefcn に対する以前の同じ呼び出しから派生する。

    • 最初の次元の特定インデックスについて、そのインデックスの入力 [a, b, c, ...] の各行は、最初の次元の同じインデックスから派生する。

  2. 出力引数 — すべての出力 [rA, rB, rC, ...] は、最初の次元が同じサイズでなければなりません。さらに、必要に応じて縮小を繰り返すことができるように、それぞれの入力 [a, b, c, ...] と垂直方向に結合可能でなければなりません。

  3. 関数ルールreducefcn は、以下の関数ルールを満たさなければなりません (丸め誤差を除く)。

    • F(input) == F(F(input)):同じ入力に繰り返し関数を適用しても、結果が変わってはならない。

    • F([input1; input2]) == F([input2; input1]):結果が、連結の順序に依存することがあってはならない。

    • F([input1; input2]) == F([F(input1); F(input2)]):いくつかの中間結果の連結に関数を 1 回適用することは、それを別々に適用し、連結し、再び適用することと同じでなければならない。

  4. 空の入力reducefcn が高さ 0 の入力を処理できることを確認します。空の入力は、ファイルが空の場合や、データに対して数多くのフィルター処理を行った場合に発生することがあります。この呼び出しでは、すべての入力ブロックは、最初の次元以外で正しい型とサイズをもつ空の配列となります。

スライディングウィンドウ操作

関数 matlab.tall.movingWindow および matlab.tall.blockMovingWindow は、tall 配列内のデータのウィンドウに関数を適用します。matlab.tall.transformmatlab.tall.reduce は一度にデータ ブロック全体に対して操作を行いますが、移動ウィンドウ関数は、ウィンドウが配列の先頭から末尾まで移動する際にデータのウインドウに対して操作を行います。ウィンドウは、ディスクから読み取られるデータの複数のブロックにまたがることができます。

以下の図は、matlab.tall.movingWindow または matlab.tall.blockMovingWindow によって配列のブロックを操作したときの動作を示しています。

操作説明

Illustration of windowed transformation, where the number of rows in each block remains constant.

ウィンドウ変換 — 各ブロックの行数は同じままですが、値が変わります。出力は、不完全なデータ ウィンドウと完全なデータ ウインドウの両方に対して実行された操作の結果を含みます。

matlab.tall.movingWindowmatlab.tall.blockMovingWindow はどちらも、'EndPoints''shrink' の既定値であるか、埋め込み値が指定されている場合にデータを変換します。両方の値により、出力と入力の最初の次元が確実に同じサイズになります。

  • A = matlab.tall.movingWindow(@mean, 100, tX) は、100 のウィンドウ サイズを使用して移動平均を計算します。

Illustration of windowed filtering, where the number of rows in each block are reduced.

ウィンドウ フィルター処理 — 不完全なデータ ウィンドウが破棄されるため、出力の要素数は入力より少なくなります。出力は、完全なデータ ウィンドウに対して行った操作の結果のみを含みます。

matlab.tall.movingWindowmatlab.tall.blockMovingWindow はどちらも 'EndPoints''discard' の場合に不完全なデータ ウィンドウを削除します。

  • A = matlab.tall.movingWindow(@mean, 100, tX, 'EndPoints', 'discard') は、100 のウィンドウ サイズを使用して完全なデータ ウィンドウの移動平均を計算します。

matlab.tall.movingWindowmatlab.tall.blockMovingWindow を使用して、データにウィンドウ変換やフィルターを適用できます。たとえば、末尾平均や移動中央値を計算したり、一度に複数の操作を同じウィンドウに対して適用することができます。2 つの関数は次の点で違いがあります。

  • matlab.tall.movingWindow は、ウィンドウが完全かどうかにかかわらず、すべてのデータ ウィンドウに fcn を適用します。matlab.tall.blockMovingWindow は、不完全なデータ ウィンドウに windowfcn を適用し、完全なデータ ウィンドウには blockfcn を適用します。

  • matlab.tall.movingWindow は一度に 1 つのデータ ウィンドウに対して操作を行います。matlab.tall.blockMovingWindow は、複数の完全なウィンドウを含んだデータ ブロック全体に対して操作を行うため、計算に必要な関数呼び出しの数が少なくなります。

移動ウィンドウの構文

移動ウィンドウ操作を 1 つのデータ ウィンドウに適用する構文は次のとおりです。

[tA, tB, tC, ...] = matlab.tall.movingWindow(fcn, window, tX, tY, tZ, ...)

fcn の関数シグネチャは次のようにしなければなりません。

[a, b, c, ...] = fcn(x, y, z, ...)

同様に、移動ウィンドウ操作をデータ ブロック全体に適用する構文は次のとおりです。

[tA, tB, tC, ...] = matlab.tall.blockMovingWindow(windowfcn, blockfcn, window, tX, tY, tZ, ...)

windowfcn および blockfcn の関数シグネチャは次のようにしなければなりません。

[a, b, c, ...] = windowfcn(info, x, y, z, ...)
[a, b, c, ...] = blockfcn(info, bX, bY, bZ, ...)

入力 info は、Window および Stride フィールドを含む構造体です。関数を作成するときに、これらのフィールドを使用して各ブロックからデータのウィンドウを選択します。

fcnwindowfcn、および blockfcn が従わなければならない一般規則の概要については、fcn の機能要件を参照してください。入力 info を除けば、fcnwindowfcn の要件は同じです。しかし、関数 blockfcn はデータ ブロック全体に対して操作を行うため、要件が異なります。

windowfcn の機能要件

windowfcn の一般的な関数シグネチャは、次のとおりです。

[a, b, c, ...] = windowfcn(info, x, y, ...)
入力 info は、matlab.tall.blockMovingWindow によって提供される構造体で、以下のフィールドを含みます。

  • Stride — ウィンドウ間の指定ステップ サイズ (既定の設定: 1)。この値は名前と値のペア 'Stride' で設定します。

  • Window — 指定のウィンドウ サイズ。この値は window 入力引数で設定します。

windowfcn は以下の要件を満たさなければなりません。

  1. 入力引数 — 入力 [x, y, z, ...] は、メモリに収まるデータのブロックです。ブロックは、tall 配列の入力 [tX, tY, tZ, ...] それぞれからデータを抽出することによって生成されます。入力 [x, y, z, ...] は次の特性を満たします。

    • すべての入力 [x, y, z, ...] は最初の次元が同じサイズとなる。

    • [x, y, z, ...] のデータ ブロックは、tall 配列の tall 次元が 1 ではないと仮定した場合、tall 次元の同じインデックスから得られる。たとえば、tXtY の tall 次元が 1 ではない場合、ブロックの最初のセットは x = tX(1:20000,:) および y = tY(1:20000,:) になる可能性があります。

    • [tX, tY, tZ, ...] のいずれかの最初の次元がサイズ 1 である場合、対応するブロック [x, y, z, ...] は、その tall 配列内のすべてのデータで構成される。

    • windowfcn を適用した結果、入力データはスカラーまたは高さ 1 の配列のスライスに削減されなければならない。

      入力が行列、N 次元配列、table、または timetable である場合、windowfcn を適用することで、その各列または各変数における入力データが削減されなければなりません。

  2. 出力引数 — 出力 [a, b, c, ...] はメモリに収まるブロックで、それぞれの出力 [tA, tB, tC, ...] に送信されます。出力 [a, b, c, ...] は以下の特性を満たします。

    • 出力 [a, b, c, ...] はすべて、最初の次元が同じサイズでなければならない。

    • 出力 [a, b, c, ...] はすべて、windowfcn の以前の呼び出しの結果とそれぞれ垂直方向に連結される。

    • 出力 [a, b, c, ...] はすべて、それぞれの出力先で、出力配列の最初の次元にある同じインデックスに送信される。

  3. 関数ルールwindowfcn は以下の関数ルールを満たしていなければなりません。

    • F([inputs1; inputs2]) == [F(inputs1); F(inputs2)]:入力の連結に関数を適用することは、入力に関数を個別に適用してから結果を連結する場合と同じでなければならない。

blockfcn の機能要件

blockfcn の一般的な関数シグネチャは、次のとおりです。

[a, b, c, ...] = blockfcn(info, bX, bY, bZ, ...)
入力 info は、matlab.tall.blockMovingWindow によって提供される構造体で、以下のフィールドを含みます。

  • Stride — ウィンドウ間の指定ステップ サイズ (既定の設定: 1)。この値は名前と値のペア 'Stride' で設定します。

  • Window — 指定のウィンドウ サイズ。この値は window 入力引数で設定します。

matlab.tall.blockMovingWindowblockfcn に提供するデータ ブロック bX, bY, bZ, ... には次の特性があります。

  • ブロックにはフルサイズのウィンドウのみが含まれる。blockfcn は不完全なデータ ウィンドウに対する動作を定義する必要はありません。

  • 最初のデータ ウィンドウは、ブロックの最初の要素から開始する。最後のウィンドウの最後の要素は、ブロックの最後の要素です。

blockfcn は以下の要件を満たさなければなりません。

  1. 入力引数 — 入力 [bX, bY, bZ, ...] は、メモリに収まるデータのブロックです。ブロックは、tall 配列の入力 [tX, tY, tZ, ...] それぞれからデータを抽出することによって生成されます。入力 [bX, bY, bZ, ...] は次の特性を満たします。

    • 入力 [bX, bY, bZ, ...] はすべて、任意の可能な拡張後の最初の次元が同じサイズをもつ。

    • [bX, bY, bZ, ...] のデータ ブロックは、tall 配列の tall 次元が 1 ではないと仮定した場合、tall 次元の同じインデックスから得られる。たとえば、tXtY の tall 次元が 1 ではない場合、ブロックの最初のセットは bX = tX(1:20000,:) および bY = tY(1:20000,:) になる可能性があります。

    • データ入力 [tX, tY, tZ, ...] のいずれかの最初の次元がサイズ 1 である場合、対応するブロック [bX, bY, bZ, ...] は、その tall 配列内のすべてのデータで構成される。

    • blockfcn を適用した結果、入力データがブロック内のウィンドウの数と等しい高さになるよう削減されなければならない。ブロック内のウィンドウの数を特定するには info.Windowinfo.Stride を使用できます。

      入力が行列、N 次元配列、table、または timetable である場合、blockfcn を適用した結果、入力データはその各列または各変数において削減されなければなりません。

  2. 出力引数 — 出力 [a, b, c, ...] はメモリに収まるブロックで、それぞれの出力 [tA, tB, tC, ...] に送信されます。出力 [a, b, c, ...] は以下の特性を満たします。

    • 出力 [a, b, c, ...] はすべて、最初の次元が同じサイズでなければならない。

    • 出力 [a, b, c, ...] はすべて、blockfcn の以前の呼び出しの結果とそれぞれ垂直方向に連結される。

    • 出力 [a, b, c, ...] はすべて、それぞれの出力先で、出力配列の最初の次元にある同じインデックスに送信される。

  3. 関数ルールblockfcn は以下の関数ルールを満たしていなければなりません。

    • F([inputs1; inputs2]) == [F(inputs1); F(inputs2)]:入力の連結に関数を適用することは、入力に関数を個別に適用してから結果を連結する場合と同じでなければならない。

出力データ型の制御

サポートされている APIのいずれかからの最終的な出力に入力と異なるデータ型がある場合は、名前と値のペア 'OutputsLike' を指定して、対応する出力と同じデータ型と属性をもつ 1 つ以上のプロトタイプ配列を提示 "しなければなりません"。'OutputsLike' の値は常に cell 配列であり、各 cell には対応する出力引数のプロトタイプ配列が含まれます。

たとえば、次の matlab.tall.transform の呼び出しは、1 つの tall 配列 tX を入力として受け取り、プロトタイプ配列 protoA および protoB によって指定された異なる型をもつ 2 つの出力を返します。出力 AprotoA と同じデータ型と属性をもち、BprotoB の場合も同様です。

C = {protoA protoB};
[A, B] = matlab.tall.transform(fcn, tX, 'OutputsLike', C)

プロトタイプ配列を提供する一般的な方法は、適切なデータ型の自明な入力で fcn を呼び出すことです。これは、fcn によって返される出力が正しいデータ型になるためです。この例では、変換関数が tall double を受け取り、tall table を返します。プロトタイプ配列は fcn(0) の呼び出しによって生成され、プロトタイプは 'OutputsLike' の値として指定されます。

ds = tabularTextDatastore('airlinesmall.csv','TreatAsMissing','NA');
ds.SelectedVariableNames = {'ArrDelay', 'DepDelay'};
tt = tall(ds);
tX = tt.ArrDelay;

fcn = @(x) table(x,'VariableNames',{'MyVar'});
proto_A = fcn(0);
A = matlab.tall.transform(fcn,tX,'OutputsLike',{proto_A});

コーディングとパフォーマンスのヒント

  • 不必要な入れ子関数を使用するのではなく、1 つの関数にすべての解析を組み込んで呼び出し、データを直接操作します。

  • データの小さなサブセットを使用して実験します。データ セット全体にスケール アップするとボトルネックが強く拡大されて現れる可能性があるため、その前にコードをプロファイリングしてボトルネックを見つけ、修正します。

  • 一部の関数は入力データに応じて異なる形状の出力を返すため、データの方向に注意してください。たとえば、unique は入力データの方向に応じて、行ベクトルまたは列ベクトルのいずれかを返すことができます。

  • ブロックは、使用可能なコンピューター メモリに基づいて実行時に動的に生成されます。指定されたすべてのリダクション関数が、関数ルール F([input1; input2]) == F([F(input1); F(input2)]) に従っていることを確認してください。このルールに従っていない場合、結果は試行間で大きく異なってくることがあります。

  • ブロックの最初の次元は、0 や 1 を含め任意のサイズにできます。サイズ 0 または 1 は、フィルター処理やリダクション演算の結果として中間計算において発生することがあります。関数が、これら両方のケースで正しく動作することを確認してください。関数がこれらのケースを正しく処理しない兆候の 1 つは、"出力のサイズが異なります" のエラー メッセージを受信する場合です。

参考

| | |

関連するトピック