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

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

「水曜日」シーズン1の中心には大きなミステリーがあります

「水曜日」シーズン1の中心には大きなミステリーがあります

Netflixの「水曜日」は、典型的な10代のドラマ以上のものであり、実際、シーズン1にはその中心に大きなミステリーがあります.

ボディーランゲージの専門家は、州訪問中にカミラ・パーカー・ボウルズが輝くことを可能にした微妙なケイト・ミドルトンの動きを指摘しています

ボディーランゲージの専門家は、州訪問中にカミラ・パーカー・ボウルズが輝くことを可能にした微妙なケイト・ミドルトンの動きを指摘しています

ケイト・ミドルトンは、州の夕食会と州の訪問中にカミラ・パーカー・ボウルズからスポットライトを奪いたくなかった、と専門家は言う.

一部のファンがハリー・スタイルズとオリビア・ワイルドの「非常に友好的な」休憩が永続的であることを望んでいる理由

一部のファンがハリー・スタイルズとオリビア・ワイルドの「非常に友好的な」休憩が永続的であることを望んでいる理由

一部のファンが、オリビア・ワイルドが彼女とハリー・スタイルズとの間の「難しい」が「非常に友好的」な分割を恒久的にすることを望んでいる理由を見つけてください.

エリザベス女王の死後、ケイト・ミドルトンはまだ「非常に困難な時期」を過ごしている、と王室の専門家が明らかにする 

エリザベス女王の死後、ケイト・ミドルトンはまだ「非常に困難な時期」を過ごしている、と王室の専門家が明らかにする&nbsp;

エリザベス女王の死後、ケイト・ミドルトンが舞台裏で「非常に困難な時期」を過ごしていたと伝えられている理由を調べてください.

セントヘレナのジェイコブのはしごを登るのは、気弱な人向けではありません

セントヘレナのジェイコブのはしごを登るのは、気弱な人向けではありません

セント ヘレナ島のジェイコブズ ラダーは 699 段の真っ直ぐ上る階段で、頂上に到達すると証明書が発行されるほどの難易度です。

The Secrets of Airline Travel Quiz

The Secrets of Airline Travel Quiz

Air travel is far more than getting from point A to point B safely. How much do you know about the million little details that go into flying on airplanes?

Where in the World Are You? Take our GeoGuesser Quiz

Where in the World Are You? Take our GeoGuesser Quiz

The world is a huge place, yet some GeoGuessr players know locations in mere seconds. Are you one of GeoGuessr's gifted elite? Take our quiz to find out!

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

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

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

ジェフ・トゥイーディーはトロルに直接話しかけ、「温かい怒りの料理」を約束します

ジェフ・トゥイーディーはトロルに直接話しかけ、「温かい怒りの料理」を約束します

写真:Mark Metcalfe / Getty Imagesバンドのファンに感謝している限り、JeffTweedyはWilcoのFacebookページを政治のないフォーラムに変えようとはしていません。フロントマンは昨日、ウィルコがソーシャルメディアの存在から州と世界の問題を遠ざけることを望んでいるトロールに転向した(またはおそらく彼らはいつもそうだった)コメント投稿者に応えてオンラインの文書を投稿した。

弾丸、バダス、そして赤ちゃん:ジョンウーの見事なピストルオペラはそれをすべて持っています

弾丸、バダス、そして赤ちゃん:ジョンウーの見事なピストルオペラはそれをすべて持っています

ハードボイルドジョンウーの1992年の傑作であるハードボイルドの数分後、2人のガンランナーが、人々が鳥かごを運ぶ香港の喫茶店の1つから逃げようとします。彼らは取引をしているが、彼らを破滅させるために数人のスーパーコップが現れ、彼らはただ逃げようとしている。

ローマ経済は大国でした

ローマ経済は大国でした

写真提供者:Clive Brunskill / Gettyローマ帝国のことを非常に多く覚えています。剣闘士は常に人気があり、軍隊はすぐ近くにありますが、ローマで最も印象的なのは、残された建物の記念碑的な風景です。

ホーク+ハチェットキャンドルで森を家に持ち帰る

ホーク+ハチェットキャンドルで森を家に持ち帰る

Hawk + ​​Hatchet Hawk + ​​Hatchetキャンドルは、キャンプファイヤー、キャビン、港町の思い出をとらえるために、手で注がれ、混ぜられた小さなバッチです。ろうそくは見栄えが良く、匂いもさらに良く、圧倒されることなく部屋を満たし、意図されたシーンをうまく呼び起こします。

ケイト・ミドルトンとウィリアム王子は、彼らが子供たちと行っているスパイをテーマにした活動を共有しています

ケイト・ミドルトンとウィリアム王子は、彼らが子供たちと行っているスパイをテーマにした活動を共有しています

ケイト・ミドルトンとウィリアム王子は、子供向けのパズルの本の序文を書き、ジョージ王子、シャーロット王女、ルイ王子と一緒にテキストを読むと述べた.

事故で押しつぶされたスイカは、動物を喜ばせ水分補給するために野生生物保護団体に寄付されました

事故で押しつぶされたスイカは、動物を喜ばせ水分補給するために野生生物保護団体に寄付されました

Yak's Produce は、数十個のつぶれたメロンを野生動物のリハビリ専門家であるレスリー グリーンと彼女のルイジアナ州の救助施設で暮らす 42 匹の動物に寄付しました。

デミ・ロヴァートは、新しいミュージシャンのボーイフレンドと「幸せで健康的な関係」にあります: ソース

デミ・ロヴァートは、新しいミュージシャンのボーイフレンドと「幸せで健康的な関係」にあります: ソース

8 枚目のスタジオ アルバムのリリースに向けて準備を進めているデミ ロヴァートは、「スーパー グレート ガイ」と付き合っている、と情報筋は PEOPLE に確認しています。

Plathville の Kim と Olivia Plath が数年ぶりに言葉を交わすことへようこそ

Plathville の Kim と Olivia Plath が数年ぶりに言葉を交わすことへようこそ

イーサン プラスの誕生日のお祝いは、TLC のウェルカム トゥ プラスビルのシーズン 4 のフィナーレで、戦争中の母親のキム プラスと妻のオリビア プラスを結びつけました。

安全な公衆パニックルーム

安全な公衆パニックルーム

不気味な音に慣れて、コンクリートの周りをうろつきます。痛みや苦い口を吐き出し、「過ち」の不思議な病気を通してもう一度味わうことを切望します。

長い間行方不明だった 2 つのサンフランシスコ クリークが間もなく日の目を見る可能性がある

都市の水路を回復する理由はたくさんありますが、気候が変化するにつれて、それらはすべてより持続可能な未来につながります.

長い間行方不明だった 2 つのサンフランシスコ クリークが間もなく日の目を見る可能性がある

サンフランシスコの長い間失われた小川を復元する動きが高まっており、コミュニティを地域の生態系と結びつけ、また、都市が気候変動や季節的な洪水に適応するのに役立つ新しいタイプの「グリーン インフラストラクチャ」を作成しています。最新の取り組みは町の 2 つの非常に異なる場所で行われており、それらは明確ではあるが補完的な方法で展開されています。

サンフランシスコのベイエリアで家族写真が撮れる場所トップ 5

サンフランシスコのベイエリアで家族写真が撮れる場所トップ 5

ベイエリアには写真を撮るための美しい屋外スポットがたくさんあります。この記事のすべての写真は、ベイエリアにいるさまざまな PictureHum フォトグラファーとの PictureHum セッションからのものです。

退屈に耐えられないから生きていけない

退屈に耐えられないから生きていけない

それが現代の衝動です — より少ないもので世界を覆し、私たちのほんのわずかな瞬間を自分たちから盗もうとする. 私はかつてダイニングルームのソファで一日を過ごし、祖父母の農家に斜めに差し込む光を眺め、私には関係のない世界について話している大人のつぶやきを半分聞いたり、壁を通してくぐもったテレビを聞いたり、遊んだりしました。床と犬と私の小さな靴を暖めた太陽の黒点と。

Language