結合ループよりも個別ループの方が要素ごとの追加がはるかに速いのはなぜですか?

2286
Johannes Gerer 2011-12-18 10:40.

仮定a1b1c1、およびd1ヒープメモリと私の数値コードのポイントは、以下のコアループを有しています。

const int n = 100000;

for (int j = 0; j < n; j++) {
    a1[j] += b1[j];
    c1[j] += d1[j];
}

このループは、別の外部forループを介して10,000回実行されます。それをスピードアップするために、私はコードを次のように変更しました:

for (int j = 0; j < n; j++) {
    a1[j] += b1[j];
}

for (int j = 0; j < n; j++) {
    c1[j] += d1[j];
}

完全に最適化されたMSVisual C ++ 10.0でコンパイルされ、Intel Core 2 Duo(x64)で32ビット用にSSE2が有効になっている場合、最初の例は5.5秒かかり、ダブルループの例は1.9秒しかかかりません。私の質問は次のとおりです:(下部にある私の言い換えられた質問を参照してください)

PS:これが役立つかどうかはわかりません:

最初のループの逆アセンブルは基本的に次のようになります(このブロックはプログラム全体で約5回繰り返されます)。

movsd       xmm0,mmword ptr [edx+18h]
addsd       xmm0,mmword ptr [ecx+20h]
movsd       mmword ptr [ecx+20h],xmm0
movsd       xmm0,mmword ptr [esi+10h]
addsd       xmm0,mmword ptr [eax+30h]
movsd       mmword ptr [eax+30h],xmm0
movsd       xmm0,mmword ptr [edx+20h]
addsd       xmm0,mmword ptr [ecx+28h]
movsd       mmword ptr [ecx+28h],xmm0
movsd       xmm0,mmword ptr [esi+18h]
addsd       xmm0,mmword ptr [eax+38h]

ダブルループの例の各ループは、このコードを生成します(次のブロックは約3回繰り返されます)。

addsd       xmm0,mmword ptr [eax+28h]
movsd       mmword ptr [eax+28h],xmm0
movsd       xmm0,mmword ptr [ecx+20h]
addsd       xmm0,mmword ptr [eax+30h]
movsd       mmword ptr [eax+30h],xmm0
movsd       xmm0,mmword ptr [ecx+28h]
addsd       xmm0,mmword ptr [eax+38h]
movsd       mmword ptr [eax+38h],xmm0
movsd       xmm0,mmword ptr [ecx+30h]
addsd       xmm0,mmword ptr [eax+40h]
movsd       mmword ptr [eax+40h],xmm0

動作はアレイのサイズ(n)とCPUキャッシュに大きく依存するため、この質問は関連性がないことが判明しました。したがって、さらに関心がある場合は、質問を言い換えます。

次のグラフの5つの領域で示されているように、さまざまなキャッシュ動作につながる詳細について、確かな洞察を提供できますか?

これらのCPUに同様のグラフを提供することにより、CPU /キャッシュアーキテクチャ間の違いを指摘することも興味深いかもしれません。

PPS:これが完全なコードです。高解像度のタイミングにTBB Tick_Countを使用しますが、TBB_TIMINGマクロを定義しないことで無効にできます。

#include <iostream>
#include <iomanip>
#include <cmath>
#include <string>

//#define TBB_TIMING

#ifdef TBB_TIMING   
#include <tbb/tick_count.h>
using tbb::tick_count;
#else
#include <time.h>
#endif

using namespace std;

//#define preallocate_memory new_cont

enum { new_cont, new_sep };

double *a1, *b1, *c1, *d1;


void allo(int cont, int n)
{
    switch(cont) {
      case new_cont:
        a1 = new double[n*4];
        b1 = a1 + n;
        c1 = b1 + n;
        d1 = c1 + n;
        break;
      case new_sep:
        a1 = new double[n];
        b1 = new double[n];
        c1 = new double[n];
        d1 = new double[n];
        break;
    }

    for (int i = 0; i < n; i++) {
        a1[i] = 1.0;
        d1[i] = 1.0;
        c1[i] = 1.0;
        b1[i] = 1.0;
    }
}

void ff(int cont)
{
    switch(cont){
      case new_sep:
        delete[] b1;
        delete[] c1;
        delete[] d1;
      case new_cont:
        delete[] a1;
    }
}

double plain(int n, int m, int cont, int loops)
{
#ifndef preallocate_memory
    allo(cont,n);
#endif

#ifdef TBB_TIMING   
    tick_count t0 = tick_count::now();
#else
    clock_t start = clock();
#endif
        
    if (loops == 1) {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++){
                a1[j] += b1[j];
                c1[j] += d1[j];
            }
        }
    } else {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                a1[j] += b1[j];
            }
            for (int j = 0; j < n; j++) {
                c1[j] += d1[j];
            }
        }
    }
    double ret;

#ifdef TBB_TIMING   
    tick_count t1 = tick_count::now();
    ret = 2.0*double(n)*double(m)/(t1-t0).seconds();
#else
    clock_t end = clock();
    ret = 2.0*double(n)*double(m)/(double)(end - start) *double(CLOCKS_PER_SEC);
#endif
    
#ifndef preallocate_memory
    ff(cont);
#endif

    return ret;
}


void main()
{   
    freopen("C:\\test.csv", "w", stdout);

    char *s = " ";

    string na[2] ={"new_cont", "new_sep"};

    cout << "n";

    for (int j = 0; j < 2; j++)
        for (int i = 1; i <= 2; i++)
#ifdef preallocate_memory
            cout << s << i << "_loops_" << na[preallocate_memory];
#else
            cout << s << i << "_loops_" << na[j];
#endif
            
    cout << endl;

    long long nmax = 1000000;

#ifdef preallocate_memory
    allo(preallocate_memory, nmax);
#endif
    
    for (long long n = 1L; n < nmax; n = max(n+1, long long(n*1.2)))
    {
        const long long m = 10000000/n;
        cout << n;

        for (int j = 0; j < 2; j++)
            for (int i = 1; i <= 2; i++)
                cout << s << plain(n, m, j, i);
        cout << endl;
    }
}

(さまざまな値のFLOP / sを示しますn。)

10 answers

1716
Mysticial 2011-12-18 11:17.

これをさらに分析すると、これは(少なくとも部分的に)4ポインターのデータアライメントが原因であると思います。これにより、ある程度のキャッシュバンク/ウェイの競合が発生します。

配列をどのように割り当てるかを正しく推測した場合、配列はページ行に揃えられる可能性があります

これは、各ループ内のすべてのアクセスが同じキャッシュウェイに分類されることを意味します。ただし、Intelプロセッサには、しばらくの間、8ウェイL1キャッシュの関連付けがありました。しかし実際には、パフォーマンスは完全に均一ではありません。4ウェイへのアクセスは、2ウェイと言うよりもまだ遅いです。

編集:実際には、すべてのアレイを個別に割り当てているように見えます。通常、このような大規模な割り当てが要求されると、アロケータはOSに新しいページを要求します。したがって、ページ境界からの同じオフセットに大きな割り当てが表示される可能性が高くなります。

テストコードは次のとおりです。

int main(){
    const int n = 100000;

#ifdef ALLOCATE_SEPERATE
    double *a1 = (double*)malloc(n * sizeof(double));
    double *b1 = (double*)malloc(n * sizeof(double));
    double *c1 = (double*)malloc(n * sizeof(double));
    double *d1 = (double*)malloc(n * sizeof(double));
#else
    double *a1 = (double*)malloc(n * sizeof(double) * 4);
    double *b1 = a1 + n;
    double *c1 = b1 + n;
    double *d1 = c1 + n;
#endif

    //  Zero the data to prevent any chance of denormals.
    memset(a1,0,n * sizeof(double));
    memset(b1,0,n * sizeof(double));
    memset(c1,0,n * sizeof(double));
    memset(d1,0,n * sizeof(double));

    //  Print the addresses
    cout << a1 << endl;
    cout << b1 << endl;
    cout << c1 << endl;
    cout << d1 << endl;

    clock_t start = clock();

    int c = 0;
    while (c++ < 10000){

#if ONE_LOOP
        for(int j=0;j<n;j++){
            a1[j] += b1[j];
            c1[j] += d1[j];
        }
#else
        for(int j=0;j<n;j++){
            a1[j] += b1[j];
        }
        for(int j=0;j<n;j++){
            c1[j] += d1[j];
        }
#endif

    }
    
    clock_t end = clock();
    cout << "seconds = " << (double)(end - start) / CLOCKS_PER_SEC << endl;

    system("pause");
    return 0;
}

ベンチマーク結果:

編集:実際のCore 2アーキテクチャマシンでの結果:

2 x Intel Xeon X5482 Harpertown @ 3.2 GHz:

#define ALLOCATE_SEPERATE
#define ONE_LOOP
00600020
006D0020
007A0020
00870020
seconds = 6.206

#define ALLOCATE_SEPERATE
//#define ONE_LOOP
005E0020
006B0020
00780020
00850020
seconds = 2.116

//#define ALLOCATE_SEPERATE
#define ONE_LOOP
00570020
00633520
006F6A20
007B9F20
seconds = 1.894

//#define ALLOCATE_SEPERATE
//#define ONE_LOOP
008C0020
00983520
00A46A20
00B09F20
seconds = 1.993

観察:

  • 6.206秒1つのループとし、2.116秒2つのループを有します。これにより、OPの結果が正確に再現されます。

  • 最初の2つのテストでは、アレイは個別に割り当てられます。それらはすべて、ページに対して同じ配置になっていることに気付くでしょう。

  • 次の2つのテストでは、配列をまとめてパックし、その配置を解除します。ここでは、両方のループが高速であることがわかります。さらに、2番目の(ダブル)ループは、通常予想されるように遅いループになりました。

@Stephen Cannonがコメントで指摘しているように、この配置により、ロード/ストアユニットまたはキャッシュで誤ったエイリアシングが発生する可能性が非常に高くなります。私はこれをグーグルで調べたところ、Intelには実際には部分的なアドレスエイリアシングストール用のハードウェアカウンターがあることがわかりました。

http://software.intel.com/sites/products/documentation/doclib/stdxe/2013/~amplifierxe/pmw_dp/events/partial_address_alias.html


5つの地域-説明

リージョン1:

これは簡単です。データセットは非常に小さいため、パフォーマンスはループや分岐などのオーバーヘッドによって支配されます。

リージョン2:

ここで、データサイズが大きくなると、相対的なオーバーヘッドの量が減少し、パフォーマンスが「飽和」します。ここでは、ループと分岐のオーバーヘッドが2倍になるため、2つのループは遅くなります。

ここで何が起こっているのか正確にはわかりません... Agner Fogがキャッシュバンクの競合について言及しているため、調整は依然として効果を発揮する可能性があります。(そのリンクはSandy Bridgeに関するものですが、このアイデアはCore 2にも適用できるはずです。)

リージョン3:

この時点で、データはL1キャッシュに収まりません。したがって、パフォーマンスはL1 <-> L2キャッシュ帯域幅によって制限されます。

リージョン4:

シングルループでのパフォーマンスの低下は、私たちが観察していることです。そして前述のように、これは(ほとんどの場合)プロセッサのロード/ストアユニットで誤ったエイリアシングストールを引き起こすアライメントによるものです。

ただし、誤ったエイリアシングが発生するためには、データセット間に十分な大きさのストライドが必要です。これが、リージョン3でこれが表示されない理由です。

リージョン5:

この時点では、キャッシュに収まるものはありません。したがって、メモリ帯域幅に制限されます。


230
Johannes Gerer 2011-12-18 15:29.

OK、正しい答えは間違いなくCPUキャッシュで何かをしなければなりません。ただし、キャッシュ引数を使用することは、特にデータがないと非常に難しい場合があります。

多くの答えがあり、それが多くの議論につながりましたが、それに直面しましょう。キャッシュの問題は非常に複雑になる可能性があり、一次元ではありません。それらはデータのサイズに大きく依存するため、私の質問は不公平でした。キャッシュグラフの非常に興味深いポイントにあることが判明しました。

@Mysticialの答えは、多くの人々(私を含む)を納得させました。おそらくそれが事実に依存しているように見えた唯一の人だったからですが、それは真実の1つの「データポイント」にすぎませんでした。

そのため、私は彼のテスト(連続割り当てと個別割り当てを使用)と@ James'Answerのアドバイスを組み合わせました。

以下のグラフは、使用される正確なシナリオとパラメーターに応じて、ほとんどの回答、特に質問と回答に対するコメントの大部分が完全に間違っているか正しいと見なされる可能性があることを示しています。

私の最初の質問はn = 100.000であったことに注意してください。この点は(偶然に)特別な振る舞いを示します:

  1. 1ループバージョンと2ループバージョンの間で最大の不一致があります(ほぼ3倍)

  2. これは、1ループ(つまり、連続割り当て)が2ループバージョンよりも優れている唯一のポイントです。(これにより、Mysticialの答えが可能になりました。)

初期化されたデータを使用した結果:

初期化されていないデータを使用した結果(これはMysticialがテストしたものです):

そして、これは説明が難しいものです。初期化されたデータ。一度割り当てられ、異なるベクトルサイズの後続のすべてのテストケースで再利用されます。

提案

キャッシュ関連データサイズの全範囲のMFLOPS情報を提供するには、StackOverflowに関するすべての低レベルのパフォーマンス関連の質問が必要です。答えを考え、特にこの情報なしで他の人と話し合うのは、みんなの時間の無駄です。

82
Puppy 2011-12-18 10:47.

2番目のループでは、キャッシュアクティビティが大幅に少ないため、プロセッサがメモリの需要に対応しやすくなります。

51
OldCurmudgeon 2011-12-18 15:36.

n一度に2つのアレイをメモリに保持できるだけの適切な値であるマシンで作業していると想像してください。ただし、ディスクキャッシュを介して使用可能なメモリの合計は、4つすべてを保持するのに十分でした。

単純なLIFOキャッシングポリシーを想定すると、次のコードがあります。

for(int j=0;j<n;j++){
    a[j] += b[j];
}
for(int j=0;j<n;j++){
    c[j] += d[j];
}

最初の原因となるabRAMにロードされ、その後、RAMに完全に働いていたこと。また、第2のループを開始し、cそしてdその後、RAMにディスクからロードされて操作されることになります。

もう一方のループ

for(int j=0;j<n;j++){
    a[j] += b[j];
    c[j] += d[j];
}

ループを回るたびに、2つの配列をページアウトし、他の2つの配列をページインします。これは明らかにはるかに遅くなります。

テストではおそらくディスクキャッシングは見られませんが、他の形式のキャッシングの副作用が見られる可能性があります。


ここでは少し混乱や誤解があるようですので、例を使って少し詳しく説明します。

たとえばn = 2、バイトを処理しています。したがって、私のシナリオでは、RAMが4バイトしかないため、残りのメモリは大幅に遅くなります(たとえば、アクセスが100倍長くなります)。

バイトがキャッシュにない場合のかなり馬鹿げたキャッシュポリシーを想定して、そこに置き、次のバイトも取得します。次のようなシナリオが発生ます。

  • for(int j=0;j<n;j++){
     a[j] += b[j];
    }
    for(int j=0;j<n;j++){
     c[j] += d[j];
    }
    
  • キャッシュa[0]a[1]、その後b[0]b[1]セットa[0] = a[0] + b[0]キャッシュには-そこに今、キャッシュ内の4バイトある、a[0], a[1]b[0], b[1]。コスト= 100 +100。

  • a[1] = a[1] + b[1]キャッシュに設定します。コスト= 1 +1。
  • 繰り返しcd
  • 総費用= (100 + 100 + 1 + 1) * 2 = 404

  • for(int j=0;j<n;j++){
     a[j] += b[j];
     c[j] += d[j];
    }
    
  • キャッシュa[0]a[1]、その後b[0]b[1]セットa[0] = a[0] + b[0]キャッシュには-そこに今、キャッシュ内の4バイトある、a[0], a[1]b[0], b[1]。コスト= 100 +100。

  • イジェクトa[0], a[1], b[0], b[1]キャッシュとキャッシュからc[0]c[1]、その後d[0]d[1]セットc[0] = c[0] + d[0]キャッシュインチ コスト= 100 +100。
  • あなたは私がどこに向かっているのか見始めているのではないかと思います。
  • 総費用= (100 + 100 + 100 + 100) * 2 = 800

これは、古典的なキャッシュスラッシュシナリオです。

36
Emilio Garavaglia 2011-12-18 10:49.

これはコードが異なるためではなく、キャッシュが原因です。RAMはCPUレジスタよりも低速であり、変数が変更されるたびにRAMを書き込まないように、キャッシュメモリがCPU内にあります。ただし、キャッシュはRAMほど大きくないため、マップするのはその一部にすぎません。

最初のコードは、離れたメモリアドレスを各ループで交互に変更するため、キャッシュを無効にする必要があります。

2番目のコードは交互になりません。隣接するアドレスを2回流れるだけです。これにより、すべてのジョブがキャッシュ内で完了し、2番目のループの開始後にのみ無効になります。

23
Noname 2012-12-30 15:34.

ここで説明した結果を再現することはできません。

貧弱なベンチマークコードが原因かどうかはわかりませんが、次のコードを使用すると、2つの方法は私のマシンで互いに10%以内であり、通常、1つのループは2つよりもわずかに高速です-あなたが思うように期待します。

配列サイズは、8つのループを使用して2 ^ 16から2 ^ 24の範囲でした。+=割り当てがFPUにdoubleとして解釈されるメモリガベージを追加するように要求しないように、ソース配列を初期化するように注意しました。

私はそのようなの割り当てを置くなど、さまざまなスキームで遊んb[j]d[j]InitToZero[j]ループの内側、そしてまた、使用して+= b[j] = 1+= d[j] = 1、私はかなり一貫性のある結果を得ました。

ご想像のとおり、初期化bdループ内で使用すると、InitToZero[j]それらが割り当て前に背中合わせに行われたとして、複合的なアプローチに利点を与えたac、まだ10%以内。図に行きます。

ハードウェアは、デルのXPS 8500世代3とコアi7の@ 3.4 GHzおよび8ギガバイトのメモリ。2 ^ 16から2 ^ 24の場合、8つのループを使用すると、累積時間はそれぞれ44.987と40.965でした。完全に最適化されたVisualC ++ 2010。

PS:ループをゼロにカウントダウンするように変更しましたが、組み合わせた方法の方がわずかに高速でした。頭をかいて。新しい配列のサイズとループ数に注意してください。

// MemBufferMystery.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include <cmath>
#include <string>
#include <time.h>

#define  dbl    double
#define  MAX_ARRAY_SZ    262145    //16777216    // AKA (2^24)
#define  STEP_SZ           1024    //   65536    // AKA (2^16)

int _tmain(int argc, _TCHAR* argv[]) {
    long i, j, ArraySz = 0,  LoopKnt = 1024;
    time_t start, Cumulative_Combined = 0, Cumulative_Separate = 0;
    dbl *a = NULL, *b = NULL, *c = NULL, *d = NULL, *InitToOnes = NULL;

    a = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    b = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    c = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    d = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    InitToOnes = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    // Initialize array to 1.0 second.
    for(j = 0; j< MAX_ARRAY_SZ; j++) {
        InitToOnes[j] = 1.0;
    }

    // Increase size of arrays and time
    for(ArraySz = STEP_SZ; ArraySz<MAX_ARRAY_SZ; ArraySz += STEP_SZ) {
        a = (dbl *)realloc(a, ArraySz * sizeof(dbl));
        b = (dbl *)realloc(b, ArraySz * sizeof(dbl));
        c = (dbl *)realloc(c, ArraySz * sizeof(dbl));
        d = (dbl *)realloc(d, ArraySz * sizeof(dbl));
        // Outside the timing loop, initialize
        // b and d arrays to 1.0 sec for consistent += performance.
        memcpy((void *)b, (void *)InitToOnes, ArraySz * sizeof(dbl));
        memcpy((void *)d, (void *)InitToOnes, ArraySz * sizeof(dbl));

        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j] += b[j];
                c[j] += d[j];
            }
        }
        Cumulative_Combined += (clock()-start);
        printf("\n %6i miliseconds for combined array sizes %i and %i loops",
                (int)(clock()-start), ArraySz, LoopKnt);
        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j] += b[j];
            }
            for(j = ArraySz; j; j--) {
                c[j] += d[j];
            }
        }
        Cumulative_Separate += (clock()-start);
        printf("\n %6i miliseconds for separate array sizes %i and %i loops \n",
                (int)(clock()-start), ArraySz, LoopKnt);
    }
    printf("\n Cumulative combined array processing took %10.3f seconds",
            (dbl)(Cumulative_Combined/(dbl)CLOCKS_PER_SEC));
    printf("\n Cumulative seperate array processing took %10.3f seconds",
        (dbl)(Cumulative_Separate/(dbl)CLOCKS_PER_SEC));
    getchar();

    free(a); free(b); free(c); free(d); free(InitToOnes);
    return 0;
}

MFLOPSが関連するメトリックであると決定された理由がわかりません。アイデアはメモリアクセスに焦点を当てることだったので、浮動小数点の計算時間を最小限に抑えるようにしました。に残しましたが+=、理由はわかりません。

計算なしの直接割り当ては、メモリアクセス時間のよりクリーンなテストであり、ループカウントに関係なく均一なテストを作成します。会話の中で何かを逃したかもしれませんが、考え直す価値はあります。プラスが割り当てから除外されている場合、累積時間はそれぞれ31秒でほぼ同じです。

19
James 2011-12-18 10:52.

これは、CPUにそれほど多くのキャッシュミスがないためです(アレイデータがRAMチップから来るのを待つ必要があります)。CPUのレベル1キャッシュ(L1)、次にレベル2キャッシュ(L2)のサイズを超えて、コードにかかる時間をプロットするように、配列のサイズを継続的に調整することは興味深いことです。配列のサイズに対して実行します。グラフは、予想どおりに直線であってはなりません。

15
Guillaume Kiz 2012-08-18 05:23.

最初のループは、各変数への書き込みを交互に行います。2番目と3番目のものは、要素サイズの小さなジャンプのみを行います。

20cm離れたペンと紙で20本の十字の2本の平行線を書いてみてください。1つを終了してからもう一方の行を終了し、各行に交互に十字を書いてもう一度試してください。

8
Francis Cugler 2017-01-31 04:00.

元の質問

1つのループが2つのループよりも非常に遅いのはなぜですか?


結論:

ケース1は、非効率的な問題である古典的な補間問題です。また、これが、多くのマシンアーキテクチャと開発者が、並列プログラミングだけでなくマルチスレッドアプリケーションを実行できるマルチコアシステムを構築および設計することになった主な理由の1つだと思います。

ハードウェア、OS、およびコンパイラがどのように連携してRAM、キャッシュ、ページファイルなどの操作を伴うヒープ割り当てを行うかを考慮せずに、この種のアプローチからそれを見てください。これらのアルゴリズムの基礎となる数学は、これら2つのどちらがより良い解決策であるかを示しています。

労働者との間を移動しなければならないことを表すBoss存在のアナロジーを使用することができます。SummationFor LoopAB

私たちは、簡単にすることを見ることができるケース2は、高速以上ではないかのように少しの半分以上であるケース1による旅行者と労働者の間にかかった時間に必要とされている距離の差に。この計算は、BenchMark Timesと、アセンブリ命令の違いの数の両方とほぼ事実上完全に一致しています。


ここで、これらすべてがどのように機能するかを以下で説明し始めます。


問題の評価

OPのコード:

const int n=100000;

for(int j=0;j<n;j++){
    a1[j] += b1[j];
    c1[j] += d1[j];
}

そして

for(int j=0;j<n;j++){
    a1[j] += b1[j];
}
for(int j=0;j<n;j++){
    c1[j] += d1[j];
}

対価

forループの2つのバリアントに関するOPの元の質問と、キャッシュの動作に関する彼の修正された質問を、他の多くの優れた回答と有用なコメントとともに検討します。この状況と問題について別のアプローチをとることによって、ここで別のことを試みてみたいと思います。


アプローチ

2つのループと、キャッシュとページファイリングに関するすべての議論を考慮して、これを別の観点から見ることに関して、別のアプローチを取りたいと思います。キャッシュとページファイルもメモリを割り当てるための実行も含まないものです。実際、このアプローチは実際のハードウェアやソフトウェアにはまったく関係ありません。


展望

しばらくの間コードを見た後、問題が何であるか、そして何がそれを生成しているのかが非常に明らかになりました。これをアルゴリズムの問​​題に分解し、数学表記を使用する観点から見てから、数学の問題とアルゴリズムに類推を適用してみましょう。


私たちが知っていること

このループは100,000回実行されることがわかっています。我々はまた、それを知っているa1b1c1d164ビットアーキテクチャ上のポインタです。32ビットマシンのC ++内では、すべてのポインターは4バイトであり、64ビットマシンでは、ポインターは固定長であるため、サイズは8バイトです。

どちらの場合も、32バイトを割り当てることができます。唯一の違いは、各反復で32バイトまたは2セットの2〜8バイトを割り当てることです。2番目のケースでは、両方の独立したループの各反復に16バイトを割り当てます。

両方のループは、割り当ての合計で32バイトに等しくなります。この情報を使用して、次に進み、これらの概念の一般的な数学、アルゴリズム、および類推を示しましょう。

どちらの場合も、同じセットまたはグループの操作を実行する必要がある回数はわかっています。どちらの場合も、割り当てる必要のあるメモリの量はわかっています。両方のケース間の割り当ての全体的なワークロードはほぼ同じであると評価できます。


私たちが知らないこと

カウンターを設定してベンチマークテストを実行しない限り、各ケースにかかる時間はわかりません。ただし、ベンチマークは、元の質問といくつかの回答およびコメントからすでに含まれています。そして、2つの間に大きな違いが見られます。これが、この問題に対するこの提案の全体的な理由です。


調べてみよう

ヒープ割り当て、ベンチマークテスト、RAM、キャッシュ、およびページファイルを調べることで、多くの人がすでにこれを行っていることはすでに明らかです。特定のデータポイントと特定の反復インデックスを調べることも含まれており、この特定の問題に関するさまざまな会話により、多くの人々がそれに関する他の関連事項に疑問を抱き始めています。数学的アルゴリズムを使用し、それに類推を適用することによって、この問題をどのように見始めるのでしょうか。まず、いくつかのアサーションを作成します。次に、そこからアルゴリズムを構築します。


私たちの主張:

  • ループとその反復は、ループのように0で始まるのではなく、1で始まり、100000で終わる合計とします。これは、メモリアドレス指定の0インデックススキームについて心配する必要がないためです。アルゴリズム自体。
  • どちらの場合も、使用する4つの関数と2つの関数呼び出しがあり、各関数呼び出しで2つの操作が実行されます。私たちは、次のような機能への機能や通話など、これらのセットアップを設定します:F1()F2()f(a)f(b)f(c)f(d)

アルゴリズム:

1番目のケース: -1つの合計のみですが、2つの独立した関数呼び出し。

Sum n=1 : [1,100000] = F1(), F2();
                       F1() = { f(a) = f(a) + f(b); }
                       F2() = { f(c) = f(c) + f(d); }

2番目のケース: -2つの合計ですが、それぞれに独自の関数呼び出しがあります。

Sum1 n=1 : [1,100000] = F1();
                        F1() = { f(a) = f(a) + f(b); }

Sum2 n=1 : [1,100000] = F1();
                        F1() = { f(c) = f(c) + f(d); }

fromにF2()のみ存在することに気付いた場合は、fromとfromの両方とfromに含まれています。これは、後で2番目のアルゴリズム内で行われている最適化があると結論付け始めたときに明らかになります。SumCase1F1()SumCase1Sum1Sum2Case2

最初のケースを通じて、反復Sum呼び出しf(a)自己に追加されますf(b)、それは呼び出すf(c)ことは同じことを行うが、追加するf(d)ごとにそれ自体に100000、反復。後者の場合、我々は持っているSum1Sum2、彼らは2回連続で呼び出されている同じ機能であるかのように、両方同じに作用します。

このケースでは扱うことができますSum1し、Sum2単に昔ながらとしてSumどこSumこの場合、このようなルックスで:Sum n=1 : [1,100000] { f(a) = f(a) + f(b); }今、私たちはただ、それは同じ機能であることを考慮することができ、最適化のようなこのルックス。


類推による要約

2番目のケースで見たものでは、両方のforループが同じ正確な署名を持っているため、最適化があるように見えますが、これは実際の問題ではありません。問題は、によって行われている作業ではありませんf(a)f(b)f(c)、とf(d)。どちらの場合も、2つの比較でも、実行時間の違いをもたらすのは、それぞれの場合に合計が移動しなければならない距離の違いです。

考えてFor LoopsいるようSummationsであるとして反復を行い、そのBoss二人に命令を与えていることAB、そのジョブが肉にしていることCD、それぞれ、そこからいくつかのパッケージをピックアップし、それを返すように。この例えでは、forループまたは合計の反復と条件チェック自体は、実際にはを表していませんBoss。何実際に表すことBoss直接実際の数学的アルゴリズムからではなく、実際の概念からScope及びCode Block等ルーチンまたはサブルーチン内の、方法、機能、翻訳部、最初のアルゴリズムは、第2のアルゴリズムは、2つの連続範囲を有する1つの範囲を有します。

各コールスリップの最初のケースでは、にBoss行きA、注文を出し、パッケージAをフェッチするために出発しB'sます。次に、にBoss行きC、注文を出し、同じことを行い、D各反復でパッケージを受け取ります。

2番目のケースでは、すべてのパッケージが受信されるまで、パッケージを移動してフェッチするためにBoss直接動作します。次に、で動作して、すべてのパッケージを取得するために同じことを行います。AB'sBossCD's

8バイトのポインタを使用してヒープ割り当てを処理しているので、次の問題について考えてみましょう。がBossから100フィート、がから500フィートであるAとしましょう。実行の順序のために、が最初にどれだけ離れているかを心配する必要はありません。どちらの場合も、最初は最初からに移動します。このアナロジーは、この距離が正確であると言っているのではありません。これは、アルゴリズムの動作を示すための便利なテストケースシナリオです。ACBossCBossAB

多くの場合、ヒープ割り当てを実行し、キャッシュファイルとページファイルを操作する場合、アドレス位置間のこれらの距離はそれほど変化しないか、データ型の性質と配列サイズに応じて大幅に変化する可能性があります。


テストケース:

最初のケースは:最初の反復ではBoss、当初に注文票を与えるために100フィートを行かなければならないAA消灯し、彼のことをしますが、その後Bossする500フィートを移動しなければならないC彼に彼の注文票を得ました。次に、次の反復とその後の1つおきの反復Bossで、2つの間を500フィート前後に移動する必要があります。

後者の場合は:Bossへの最初の反復で100フィートを移動しなければならないAが、その後、彼はすでに存在しているとのためにちょうど待ってA、すべての伝票が満たされるまで戻って取得します。次に、Bossはから500フィートであるCため、最初の反復で500フィート移動する必要CがありAます。これBoss( Summation, For Loop )は作業直後に呼び出されるため、注文伝票がすべて完了するまでA、彼と同じようにそこで待機します。AC's


移動距離の違い

const n = 100000
distTraveledOfFirst = (100 + 500) + ((n-1)*(500 + 500); 
// Simplify
distTraveledOfFirst = 600 + (99999*100);
distTraveledOfFirst = 600 + 9999900;
distTraveledOfFirst =  10000500;
// Distance Traveled On First Algorithm = 10,000,500ft

distTraveledOfSecond = 100 + 500 = 600;
// Distance Traveled On Second Algorithm = 600ft;    

任意の値の比較

600が1,000万よりはるかに少ないことが簡単にわかります。さて、これは正確ではありません。なぜなら、RAMのどのアドレス間、または各反復での各呼び出しが他の多くの見えない変数に起因するキャッシュまたはページファイルからの距離の実際の違いがわからないからです。これは、最悪のシナリオから認識して見なければならない状況の評価にすぎません。

これらの数値から、アルゴリズム1は99%アルゴリズム2よりも遅いはずであるように見えます。しかし、これが唯一であるBoss's一部またはアルゴリズムの責任と、それが実際の労働者を考慮していないABC、&Dと彼らはそれぞれ、ループの反復ごとに行う必要があります。したがって、上司の仕事は、行われている全仕事の約15〜40%しか占めていません。労働者を介して行われる作業の大部分は、速度率の差の比率を約50〜70%に維持することに向けてわずかに大きな影響を及ぼします


観察: - 2つのアルゴリズムの間の違い

この状況では、それは行われている作業のプロセスの構造です。ケース2は、名前と移動距離が異なる変数のみである、同様の関数宣言と定義を持つ部分的な最適化の両方からより効率的であることを示しています。

また、ケース1の合計移動距離は、ケース2の場合よりもはるかに遠く、2つのアルゴリズム間のタイムファクターの移動距離を考慮することができます。ケース1には、ケース2よりもかなり多くの作業があります。

これはASM、両方の場合に示された指示の証拠から観察できます。すでにこれらの例について述べたものに加えて、これはであるという事実を考慮していないケース1のボスは、両方のために待機する必要がありますAC彼が戻って行くことができる前に戻って取得するためにA、各反復のためにもう一度。また、AまたはB非常に長い時間がかかる場合、Bossおよび他のワーカーの両方が実行されるのを待ってアイドル状態になっているという事実も考慮されていません。

ケース2つのみビーイングアイドルがあるBoss労働者が取り戻すまで。したがって、これでもアルゴリズムに影響を与えます。



OPが質問を修正しました

編集:動作は配列(n)とCPUキャッシュのサイズに大きく依存するため、質問は関連性がないことが判明しました。したがって、さらに関心がある場合は、質問を言い換えます。

次のグラフの5つの領域で示されているように、さまざまなキャッシュ動作につながる詳細について、確かな洞察を提供できますか?

これらのCPUに同様のグラフを提供することにより、CPU /キャッシュアーキテクチャ間の違いを指摘することも興味深いかもしれません。


これらの質問について

私が間違いなく示したように、ハードウェアとソフトウェアが関与する前でさえ、根本的な問題があります。

ここで、メモリとキャッシュの管理、およびページファイルなどについては、これらはすべて、次の間の統合されたシステムセットで連携して機能します。

  • The Architecture {ハードウェア、ファームウェア、一部の組み込みドライバー、カーネル、ASM命令セット}。
  • The OS{ファイルおよびメモリ管理システム、ドライバ、およびレジストリ}。
  • The Compiler {翻訳単位とソースコードの最適化}。
  • そして、Source Codeそれ自体でさえ、独特のアルゴリズムのセットを備えています。

我々はすでに私たちも任意で任意のマシンにそれを適用する前に、最初のアルゴリズムの中に起こっているボトルネックがあることがわかりますArchitectureOSと、Programmable Language第2のアルゴリズムに比べて。現代のコンピューターの本質に関わる前に、すでに問題が存在していました。


エンディング結果

しかしながら; これらの新しい質問は、それ自体が重要であり、結局のところ役割を果たすため、重要ではないということではありません。それらは手順と全体的なパフォーマンスに影響を与えます。それは、回答やコメントを提供した多くの人からのさまざまなグラフと評価から明らかです。

あなたがのアナロジーに注意を払った場合Bossと2人の労働者AB行くとからパッケージを取得しなければならなかったCD、それぞれ、問題の2つのアルゴリズムの数学的表記を考慮。コンピュータのハードウェアとソフトウェアを使用せずに、よりもCase 2ほぼ60%高速であることがわかりますCase 1

これらのアルゴリズムがいくつかのソースコードに適用され、コンパイルされ、最適化され、OSを介して実行されて特定のハードウェアで操作を実行した後のグラフとチャートを見ると、違いの間にもう少し劣化が見られます。これらのアルゴリズムで。

Dataセットがかなり小さい場合、最初はそれほど悪い違いには見えないかもしれません。ただし、Case 1は実行時間の違いの観点からこの関数の成長を見ることができる60 - 70%よりも約遅いので、Case 2次のようになります。

DeltaTimeDifference approximately = Loop1(time) - Loop2(time)
//where 
Loop1(time) = Loop2(time) + (Loop2(time)*[0.6,0.7]) // approximately
// So when we substitute this back into the difference equation we end up with 
DeltaTimeDifference approximately = (Loop2(time) + (Loop2(time)*[0.6,0.7])) - Loop2(time)
// And finally we can simplify this to
DeltaTimeDifference approximately = [0.6,0.7]*Loop2(time)

この概算は、アルゴリズムと、ソフトウェアの最適化と機械命令を含む機械操作の両方で、これら2つのループの平均差です。

データセットが直線的に増加すると、2つの間の時間差も直線的に増加します。アルゴリズム1は、場合明らかであるアルゴリズム2以上フェッチを有するBoss走行前後の間の最大距離を有しているACアルゴリズム2はながら最初の反復の後にすべての反復のためBossに移動しなければならないA一度、その後で行われた後A、彼は旅行に持っていますからに移動Aするときの最大距離は1回のみCです。

Boss同じような連続したタスクに焦点を合わせるのではなく、2つの同じようなことを一度に行い、それらを前後に動かすことに焦点を合わせようとすると、彼は2倍の旅行と仕事をしなければならなかったので、一日の終わりまでにかなり腹を立てます。したがって、上司の配偶者や子供たちがそれを評価しないので、上司を補間されたボトルネックに陥らせることによって状況の範囲を失うことはありません。



修正:ソフトウェアエンジニアリングの設計原則

-反復forループ内のLocal StackとのHeap Allocated計算の違い、およびそれらの使用法、効率、および有効性の違い-

上で提案した数学的アルゴリズムは、主にヒープに割り当てられたデータに対して操作を実行するループに適用されます。

  • 連続したスタック操作:
    • ループがスタックフレーム内にある単一のコードブロックまたはスコープ内でローカルにデータに対して操作を実行している場合でも、それは一種の適用になりますが、メモリの場所は通常シーケンシャルであり、移動距離または実行時間の違いによりはるかに近くなりますほとんど無視できます。ヒープ内で割り当てが行われていないため、メモリが分散したり、RAMを介してメモリがフェッチされたりすることはありません。通常、メモリはシーケンシャルであり、スタックフレームとスタックポインタに関連しています。
    • スタックで連続した操作が実行されている場合、最新のプロセッサは繰り返し値とアドレスをキャッシュし、これらの値をローカルキャッシュレジスタ内に保持します。ここでの操作または指示の時間は、ナノ秒のオーダーです。
  • 連続するヒープ割り当て操作:
    • ヒープ割り当ての適用を開始し、CPU、バスコントローラ、およびRamモジュールのアーキテクチャに応じて、プロセッサが連続した呼び出しでメモリアドレスをフェッチする必要がある場合、操作または実行の時間はマイクロからミリ秒。キャッシュされたスタック操作と比較すると、これらは非常に低速です。
    • CPUはRamからメモリアドレスをフェッチする必要があり、通常、システムバス全体のすべてのものは、CPU自体の内部データパスまたはデータバスと比較して低速です。

したがって、ヒープ上にある必要のあるデータを処理していて、それらをループでトラバースしている場合は、各データセットとそれに対応するアルゴリズムを独自の単一ループ内に保持する方が効率的です。ヒープ上にある異なるデータセットの複数の操作を単一のループに入れることで、連続するループを除外しようとするよりも優れた最適化が得られます。

スタック上にあるデータは頻繁にキャッシュされるため、これを行うことは問題ありませんが、反復ごとにメモリアドレスを照会する必要があるデータについてはそうではありません。

ここで、ソフトウェアエンジニアリングとソフトウェアアーキテクチャ設計が役立ちます。これは、データを整理する方法、データをキャッシュするタイミング、ヒープにデータを割り当てるタイミング、アルゴリズムを設計および実装する方法、およびそれらを呼び出すタイミングと場所を知る能力です。

同じデータセットに関連する同じアルゴリズムをO(n)使用している場合でも、作業時のアルゴリズムの複雑さからわかる上記の問題のために、スタックバリアント用とヒープ割り当てバリアント用の実装設計が必要になる場合があります。ヒープ付き。

私が何年にもわたって気づいたことから、多くの人々はこの事実を考慮に入れていません。彼らは特定のデータセットで機能する1つのアルゴリズムを設計する傾向があり、スタックにローカルにキャッシュされているデータセットやヒープに割り当てられているデータセットに関係なくそれを使用します。

真の最適化が必要な場合は、コードの重複のように見えるかもしれませんが、一般化するには、同じアルゴリズムの2つのバリアントを使用する方が効率的です。1つはスタック操作用で、もう1つは反復ループで実行されるヒープ操作用です。

疑似例を次に示します。2つの単純な構造体、1つのアルゴリズム。

struct A {
    int data;
    A() : data{0}{}
    A(int a) : data{a}{} 
};
struct B {
    int data;
    B() : data{0}{}
    A(int b) : data{b}{}
}                

template<typename T>
void Foo( T& t ) {
    // do something with t
}

// some looping operation: first stack then heap.

// stack data:
A dataSetA[10] = {};
B dataSetB[10] = {};

// For stack operations this is okay and efficient
for (int i = 0; i < 10; i++ ) {
   Foo(dataSetA[i]);
   Foo(dataSetB[i]);
}

// If the above two were on the heap then performing
// the same algorithm to both within the same loop
// will create that bottleneck
A* dataSetA = new [] A();
B* dataSetB = new [] B();
for ( int i = 0; i < 10; i++ ) {
    Foo(dataSetA[i]); // dataSetA is on the heap here
    Foo(dataSetB[i]); // dataSetB is on the heap here
} // this will be inefficient.

// To improve the efficiency above, put them into separate loops... 

for (int i = 0; i < 10; i++ ) {
    Foo(dataSetA[i]);
}
for (int i = 0; i < 10; i++ ) {
    Foo(dataSetB[i]);
}
// This will be much more efficient than above.
// The code isn't perfect syntax, it's only psuedo code
// to illustrate a point.

これは、スタックバリアントとヒープバリアントを別々に実装することで私が言及していたものです。アルゴリズム自体はそれほど重要ではありません。そこで使用するのはループ構造です。

2
mathengineer 2018-07-11 21:00.

古いC ++と最適化である可能性があります。私のコンピューターでは、ほぼ同じ速度が得られました。

1つのループ:1.577ミリ秒

2つのループ:1.507ミリ秒

Visual Studio 2015は、16 GBRAMを搭載したE5-16203.5GHzプロセッサで実行しています。

Related questions

MORE COOL STUFF

ケイト・ブランシェットは3日間一緒に夫と一緒に寝て、25年経ってもまだ夫と結婚しています

ケイト・ブランシェットは3日間一緒に夫と一緒に寝て、25年経ってもまだ夫と結婚しています

ケイト・ブランシェットは、夫に会ったとき、典型的な交際のアドバイスに逆らいました。

マイケルシーンが非営利の俳優である理由

マイケルシーンが非営利の俳優である理由

マイケルシーンは非営利の俳優ですが、それは正確にはどういう意味ですか?

ホールマークスターのコリンエッグレスフィールドがRomaDramaLiveでスリル満点のファンと出会う![エクスクルーシブ]

ホールマークスターのコリンエッグレスフィールドがRomaDramaLiveでスリル満点のファンと出会う![エクスクルーシブ]

特徴的なスターのコリン・エッグレスフィールドは、RomaDrama Liveでのスリル満点のファンとの出会いについて料理しました!加えて、大会での彼のINSPIREプログラム。

「たどりつけば」をオンラインでストリーミングできない理由

「たどりつけば」をオンラインでストリーミングできない理由

ノーザンエクスポージャーが90年代の最も人気のある番組の1つになった理由を確認するには、Blu-rayまたはDVDプレーヤーをほこりで払う必要があります。

バイオニック読書はあなたをより速く読むことができますか?

バイオニック読書はあなたをより速く読むことができますか?

BionicReadingアプリの人気が爆発的に高まっています。しかし、それは本当にあなたを速読術にすることができますか?

ドミニカのボイリング湖:アクセスは簡単ではありませんが、ハイキングする価値があります

ドミニカのボイリング湖:アクセスは簡単ではありませんが、ハイキングする価値があります

ドミニカのボイリング湖は、世界で2番目に大きいボイリング湖です。そこにたどり着くまでのトレッキングは大変で長いですが、努力する価値は十分にあります。

私たちの水をきれいに保つのを助けるためにあなたの髪を寄付してください

私たちの水をきれいに保つのを助けるためにあなたの髪を寄付してください

サロンからのヘアトリミングや個人的な寄付は、油流出を吸収して環境を保護するのに役立つマットとして再利用できます。

ホワイトハウスの最も記憶に残る結婚式を見てください

ホワイトハウスの最も記憶に残る結婚式を見てください

過去200年以上の間にホワイトハウスで結婚したのはほんの数人です。彼らは誰でしたか、そしてそこで結婚式を獲得するために何が必要ですか?

グッドジョブ、ESPN

グッドジョブ、ESPN

今日のホワイトハウスの記者会見で、報道官のサラ・ハッカビー・サンダースは、スポーツセンターのホストであるイェメル・ヒルのドナルド・トランプに関する最近のツイートについてコメントするよう求められ、大統領と彼の政策を白人至上主義者として説明した。ヒルは彼女のつぶやきのためにESPNによって公に叱責されました。

アマゾンからのこのレミントンツールセールで髪を整える、スタイルを整える、乾かす、または取り除く

アマゾンからのこのレミントンツールセールで髪を整える、スタイルを整える、乾かす、または取り除く

基本的に頭のあらゆる部分から髪の毛を整えたり、ブロードライしたり、まっすぐにしたり、脱毛したりする必要がある場合は、このレミントンゴールドボックスが最適です。今日だけ、Amazonは、すでに人気のあるShortcut Pro Self-HaircutKitやPearlPro Ceramic Flat Ironのように、グルーミングをはるかに簡単にするヘアツールをマークダウンしています。

カナダの元桂冠詩人が、史上最高の文化の盗用でトゥパックとマヤアンジェロウから盗んだ

カナダの元桂冠詩人が、史上最高の文化の盗用でトゥパックとマヤアンジェロウから盗んだ

トゥパックシャクール(ティムモーゼンフェルダー/ゲッティイメージズ); マヤアンジェロウ(マーティンゴッドウィン/ゲッティイメージズ)移動、テイラースウィフト。ケンドールとカイリーは、必要な数の座席を持っています。

テスラは、ハリケーンイルマによる避難を容易にするために、フロリダでの車両の範囲を拡大しています

テスラは、ハリケーンイルマによる避難を容易にするために、フロリダでの車両の範囲を拡大しています

写真:Tesla Motorsフロリダに住んでいて、Tesla Model S、Model X 60、またはModel 60Dを使用している場合、車の自律性は50 km(約30マイル)高くなります。これは失敗ではなく、ハリケーンイルマによる避難作業を容易にするために会社自身が命じた自治権の一時的な延長です。

Zendaya Wishes Boyfriend Tom Holland Happy Birthday with Cuddly Photo: He 'Makes Me the Happiest'

Zendaya Wishes Boyfriend Tom Holland Happy Birthday with Cuddly Photo: He 'Makes Me the Happiest'

Zendaya shared a sweet photo in honor of boyfriend Tom Holland's 26th birthday Wednesday

小さな女性:脳卒中を患った後に病院から解放されたアトランタのジューシーな赤ちゃん:「まだ癒し」

小さな女性:脳卒中を患った後に病院から解放されたアトランタのジューシーな赤ちゃん:「まだ癒し」

シーレン「Ms.JuicyBaby」ピアソンは、先月脳卒中で入院した後、「もう一度たくさんのことをする方法を学ばなければならない」ため、言語療法を受けていることを明らかにしました。

エマストーンは彼女のクリフサイドマリブビーチハウスを420万ドルでリストアップしています—中を見てください!

エマストーンは彼女のクリフサイドマリブビーチハウスを420万ドルでリストアップしています—中を見てください!

オスカー受賞者の世紀半ばの家には、3つのベッドルーム、2つのバス、オーシャンフロントの景色があります。

ジーニー・メイ・ジェンキンスは、母乳育児の経験の中で、彼女は「本当に、本当に落ち込んでいる」と言います

ジーニー・メイ・ジェンキンスは、母乳育児の経験の中で、彼女は「本当に、本当に落ち込んでいる」と言います

ジーニー・メイ・ジェンキンスは、生後4か月の娘、モナコに母乳育児をしていると語った。

投資ノート:Bioscout AU$300万シード

投資ノート:Bioscout AU$300万シード

Bioscoutは、農家を運転席に置くという使命を負っています。Artesian(GrainInnovate)やUniseedと並んで、最新のシードラウンドでチームを支援できることをうれしく思います。問題真菌症による重大な作物の損失は、農民にとって試練であることが証明されています。

リトルマーケットリサーチ1| 2022年のクイックグリンプス遠隔医療市場

リトルマーケットリサーチ1| 2022年のクイックグリンプス遠隔医療市場

遠隔医療は、パンデミック後の時代では新しいものではなく、時代遅れの分野でもありません。しかし、業界を詳しく見ると、需要と供給の強力な持続可能性と、米国で絶え間ない革命となる強力な潜在的成長曲線を示しています。

スタートアップ資金調達環境:タイのスタートアップエコシステムの次は何ですか?

スタートアップ資金調達環境:タイのスタートアップエコシステムの次は何ですか?

2021年は、世界的なベンチャーキャピタル(VC)の資金調達にとって記録的な年でした。DealStreetAsiaによると、東南アジアも例外ではなく、この地域では年間で記録的な25の新しいユニコーンが採掘されました。

ムーアの法則を超えて

ムーアの法則を超えて

計算に対する私たちの欲求とムーアの法則が提供できるものとの間には、指数関数的に増大するギャップがあります。私たちの文明は計算に基づいています—建築と想像力の現在の限界を超える技術を見つけなければなりません。

Language