Tại sao các phép cộng theo từng phần tử trong các vòng riêng biệt nhanh hơn nhiều so với trong một vòng lặp kết hợp?

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

Giả sử a1, b1, c1, và d1điểm đến bộ nhớ heap và mã số của tôi có vòng lặp cốt lõi sau đây.

const int n = 100000;

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

Vòng lặp này được thực hiện 10.000 lần thông qua một forvòng lặp bên ngoài khác . Để tăng tốc, tôi đã thay đổi mã thành:

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

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

Được biên dịch trên MS Visual C ++ 10.0 với tối ưu hóa đầy đủ và SSE2 được bật cho 32 bit trên Intel Core 2 Duo (x64), ví dụ đầu tiên mất 5,5 giây và ví dụ vòng lặp kép chỉ mất 1,9 giây. Câu hỏi của tôi là: (Vui lòng tham khảo câu hỏi được diễn đạt lại của tôi ở phía dưới)

Tái bút: Tôi không chắc, nếu điều này giúp:

Tháo gỡ cho vòng lặp đầu tiên về cơ bản trông như thế này (khối này được lặp lại khoảng năm lần trong chương trình đầy đủ):

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]

Mỗi vòng lặp của ví dụ vòng lặp kép tạo ra mã này (khối sau được lặp lại khoảng ba lần):

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

Câu hỏi hóa ra không liên quan, vì hành vi phụ thuộc nhiều vào kích thước của mảng (n) và bộ nhớ cache của CPU. Vì vậy, nếu có thêm sự quan tâm, tôi nói lại câu hỏi:

Bạn có thể cung cấp một số thông tin chi tiết chắc chắn dẫn đến các hành vi bộ nhớ cache khác nhau như được minh họa bởi năm vùng trên biểu đồ sau không?

Cũng có thể thú vị khi chỉ ra sự khác biệt giữa các kiến ​​trúc CPU / bộ đệm, bằng cách cung cấp một biểu đồ tương tự cho các CPU này.

PPS: Đây là mã đầy đủ. Nó sử dụng TBB Tick_Count cho thời gian phân giải cao hơn, có thể bị vô hiệu hóa bằng cách không xác định TBB_TIMINGMacro:

#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;
    }
}

(Nó hiển thị FLOP / s cho các giá trị khác nhau của n.)

10 answers

1716
Mysticial 2011-12-18 11:17.

Sau khi phân tích sâu hơn về điều này, tôi tin rằng điều này (ít nhất là một phần) do sự liên kết dữ liệu của bốn con trỏ. Điều này sẽ gây ra một số xung đột về ngân hàng bộ đệm / cách.

Nếu tôi đã đoán đúng về cách bạn phân bổ các mảng của mình, chúng có khả năng được căn chỉnh với dòng trang .

Điều này có nghĩa là tất cả các truy cập của bạn trong mỗi vòng lặp sẽ nằm trên cùng một cách bộ nhớ cache. Tuy nhiên, bộ vi xử lý Intel đã có khả năng liên kết bộ nhớ cache L1 8 chiều trong một thời gian. Nhưng trên thực tế, hiệu suất không hoàn toàn đồng đều. Truy cập 4 chiều vẫn chậm hơn so với truy cập 2 chiều.

CHỈNH SỬA: Trên thực tế, nó trông giống như bạn đang phân bổ tất cả các mảng một cách riêng biệt. Thông thường khi yêu cầu phân bổ lớn như vậy, trình phân bổ sẽ yêu cầu các trang mới từ Hệ điều hành. Do đó, có nhiều khả năng các phân bổ lớn sẽ xuất hiện ở cùng một khoảng chênh lệch so với ranh giới trang.

Đây là mã kiểm tra:

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;
}

Kết quả điểm chuẩn:

CHỈNH SỬA: Kết quả trên máy kiến ​​trúc Core 2 thực tế :

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

Quan sát:

  • 6,206 giây với một vòng và 2,116 giây với hai vòng. Điều này tái tạo chính xác kết quả của OP.

  • Trong hai thử nghiệm đầu tiên, các mảng được phân bổ riêng biệt. Bạn sẽ nhận thấy rằng tất cả chúng đều có cùng một căn chỉnh so với trang.

  • Trong hai thử nghiệm thứ hai, các mảng được đóng gói lại với nhau để phá vỡ sự liên kết đó. Ở đây bạn sẽ nhận thấy cả hai vòng đều nhanh hơn. Hơn nữa, vòng lặp thứ hai (kép) bây giờ là vòng lặp chậm hơn như bạn thường mong đợi.

Như @Stephen Cannon đã chỉ ra trong các nhận xét, rất có thể sự liên kết này gây ra răng cưa sai trong các đơn vị tải / lưu trữ hoặc bộ nhớ cache. Tôi đã tìm kiếm điều này trên Google và thấy rằng Intel thực sự có một bộ đếm phần cứng cho các gian hàng răng cưa địa chỉ một phần :

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


5 Khu vực - Giải thích

Vùng 1:

Điều này là dễ dàng. Tập dữ liệu quá nhỏ nên hiệu suất bị chi phối bởi chi phí như lặp và phân nhánh.

Vùng 2:

Ở đây, khi kích thước dữ liệu tăng lên, lượng chi phí tương đối giảm xuống và hiệu suất "bão hòa". Ở đây hai vòng lặp chậm hơn bởi vì nó có gấp đôi số vòng lặp và chi phí phân nhánh.

Tôi không chắc chính xác điều gì đang xảy ra ở đây ... Việc căn chỉnh vẫn có thể có tác dụng khi Agner Fog đề cập đến xung đột ngân hàng bộ nhớ cache . (Liên kết đó là về Sandy Bridge, nhưng ý tưởng vẫn nên áp dụng cho Core 2.)

Vùng 3:

Tại thời điểm này, dữ liệu không còn phù hợp với bộ đệm L1 nữa. Vì vậy, hiệu suất bị giới hạn bởi băng thông bộ nhớ cache L1 <-> L2.

Vùng 4:

Sự sụt giảm hiệu suất trong vòng lặp đơn là những gì chúng tôi đang quan sát. Và như đã đề cập, điều này là do sự căn chỉnh (rất có thể) gây ra các gian hàng răng cưa sai trong các đơn vị tải / cửa hàng của bộ xử lý.

Tuy nhiên, để xảy ra hiện tượng sai răng cưa, giữa các tập dữ liệu phải có một khoảng cách đủ lớn. Đây là lý do tại sao bạn không thấy điều này trong vùng 3.

Vùng 5:

Tại thời điểm này, không có gì phù hợp trong bộ nhớ cache. Vì vậy, bạn bị ràng buộc bởi băng thông bộ nhớ.


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

OK, câu trả lời đúng chắc chắn phải làm gì đó với bộ nhớ cache của CPU. Nhưng để sử dụng đối số bộ nhớ cache có thể khá khó khăn, đặc biệt là không có dữ liệu.

Có rất nhiều câu trả lời, dẫn đến nhiều cuộc thảo luận, nhưng hãy đối mặt với nó: Các vấn đề về bộ nhớ cache có thể rất phức tạp và không phải là một chiều. Chúng phụ thuộc nhiều vào kích thước của dữ liệu, vì vậy câu hỏi của tôi không công bằng: Hóa ra nó nằm ở một điểm rất thú vị trong biểu đồ bộ nhớ cache.

Câu trả lời của @ Mysticial đã thuyết phục rất nhiều người (trong đó có tôi), có lẽ vì nó là câu duy nhất có vẻ dựa vào sự thật, nhưng nó chỉ là một "điểm dữ liệu" của sự thật.

Đó là lý do tại sao tôi kết hợp thử nghiệm của anh ấy (sử dụng phân bổ liên tục so với phân bổ riêng biệt) và lời khuyên của @James 'Answer.

Biểu đồ dưới đây cho thấy rằng hầu hết các câu trả lời và đặc biệt là phần lớn ý kiến ​​cho câu hỏi và câu trả lời có thể được coi là hoàn toàn sai hoặc đúng tùy thuộc vào kịch bản chính xác và các thông số được sử dụng.

Lưu ý rằng câu hỏi ban đầu của tôi là n = 100.000 . Điểm này (tình cờ) thể hiện hành vi đặc biệt:

  1. Nó có sự khác biệt lớn nhất giữa phiên bản lặp lại một và hai (gần như hệ số ba)

  2. Đó là điểm duy nhất, nơi một vòng lặp (cụ thể là với phân bổ liên tục) đánh bại phiên bản hai vòng. (Điều này khiến câu trả lời của Mysticial có thể thực hiện được.)

Kết quả sử dụng dữ liệu khởi tạo:

Kết quả sử dụng dữ liệu chưa được khởi tạo (đây là những gì Mysticial đã kiểm tra):

Và đây là một điều khó giải thích: Dữ liệu được khởi tạo, được cấp phát một lần và được sử dụng lại cho mọi trường hợp thử nghiệm sau có kích thước vectơ khác nhau:

Đề nghị

Mọi câu hỏi liên quan đến hiệu suất mức thấp trên Stack Overflow phải được yêu cầu cung cấp thông tin MFLOPS cho toàn bộ phạm vi kích thước dữ liệu liên quan trong bộ nhớ cache! Thật lãng phí thời gian của mọi người để nghĩ ra câu trả lời và đặc biệt là thảo luận với những người khác mà không có thông tin này.

82
Puppy 2011-12-18 10:47.

Vòng lặp thứ hai liên quan đến hoạt động bộ nhớ cache ít hơn rất nhiều, vì vậy bộ xử lý dễ dàng bắt kịp nhu cầu bộ nhớ hơn.

51
OldCurmudgeon 2011-12-18 15:36.

Hãy tưởng tượng bạn đang làm việc trên một chiếc máy có ngiá trị phù hợp để nó chỉ có thể chứa hai mảng trong bộ nhớ cùng một lúc, nhưng tổng bộ nhớ có sẵn, thông qua bộ nhớ đệm đĩa, vẫn đủ để chứa cả bốn mảng.

Giả sử một chính sách bộ nhớ đệm LIFO đơn giản, mã này:

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

trước tiên sẽ gây ra abđược tải vào RAM và sau đó được làm việc hoàn toàn trong RAM. Khi vòng lặp thứ hai bắt đầu, cdsau đó sẽ được tải từ đĩa vào RAM và hoạt động.

vòng lặp khác

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

sẽ trang ra hai mảng và trang trong hai mảng còn lại mỗi lần xung quanh vòng lặp . Điều này rõ ràng sẽ chậm hơn nhiều .

Bạn có thể không thấy bộ nhớ đệm đĩa trong các thử nghiệm của mình nhưng có thể bạn đang thấy tác dụng phụ của một số dạng bộ nhớ đệm khác.


Có vẻ như có một chút nhầm lẫn / hiểu lầm ở đây vì vậy tôi sẽ cố gắng giải thích một chút bằng cách sử dụng một ví dụ.

Nói n = 2và chúng tôi đang làm việc với byte. Trong kịch bản của tôi, do đó, chúng tôi chỉ4 byte RAM và phần còn lại của bộ nhớ của chúng tôi chậm hơn đáng kể (giả sử truy cập lâu hơn 100 lần).

Giả sử một chính sách bộ nhớ đệm khá ngu ngốc về việc nếu byte không có trong bộ nhớ cache, hãy đặt nó ở đó và lấy byte sau khi chúng tôi đang ở đó, bạn sẽ nhận được một tình huống như sau:

  • Với

    for(int j=0;j<n;j++){
     a[j] += b[j];
    }
    for(int j=0;j<n;j++){
     c[j] += d[j];
    }
    
  • bộ nhớ cache a[0]a[1]sau đó b[0]b[1]và đặt a[0] = a[0] + b[0]trong bộ nhớ cache - hiện có bốn byte trong bộ nhớ cache, a[0], a[1]b[0], b[1]. Chi phí = 100 + 100.

  • đặt a[1] = a[1] + b[1]trong bộ nhớ cache. Chi phí = 1 + 1.
  • Lặp lại cho cd.
  • Tổng chi phí = (100 + 100 + 1 + 1) * 2 = 404

  • Với

    for(int j=0;j<n;j++){
     a[j] += b[j];
     c[j] += d[j];
    }
    
  • bộ nhớ cache a[0]a[1]sau đó b[0]b[1]và đặt a[0] = a[0] + b[0]trong bộ nhớ cache - hiện có bốn byte trong bộ nhớ cache, a[0], a[1]b[0], b[1]. Chi phí = 100 + 100.

  • đẩy ra a[0], a[1], b[0], b[1]khỏi bộ đệm và bộ đệm c[0], c[1]sau đó d[0]d[1]và đặt c[0] = c[0] + d[0]trong bộ đệm. Chi phí = 100 + 100.
  • Tôi nghi ngờ bạn đang bắt đầu nhìn thấy nơi tôi sẽ đi.
  • Tổng chi phí = (100 + 100 + 100 + 100) * 2 = 800

Đây là một kịch bản hủy bộ nhớ cache cổ điển.

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

Đó không phải là do mã khác, mà là do bộ nhớ đệm: RAM chậm hơn thanh ghi của CPU và bộ nhớ đệm nằm bên trong CPU để tránh ghi RAM mỗi khi một biến thay đổi. Nhưng bộ nhớ đệm không lớn bằng RAM, do đó, nó chỉ ánh xạ một phần nhỏ của nó.

Đoạn mã đầu tiên sửa đổi các địa chỉ bộ nhớ ở xa xen kẽ chúng tại mỗi vòng lặp, do đó yêu cầu liên tục làm mất hiệu lực của bộ đệm.

Mã thứ hai không thay thế: nó chỉ chảy trên các địa chỉ liền kề hai lần. Điều này làm cho tất cả công việc được hoàn thành trong bộ đệm, chỉ làm mất hiệu lực sau khi vòng lặp thứ hai bắt đầu.

23
Noname 2012-12-30 15:34.

Tôi không thể lặp lại các kết quả được thảo luận ở đây.

Tôi không biết liệu mã điểm chuẩn kém có phải là nguyên nhân hay không, nhưng hai phương pháp nằm trong phạm vi 10% của nhau trên máy của tôi bằng cách sử dụng mã sau và một vòng lặp thường nhanh hơn một chút - như bạn muốn chờ đợi.

Kích thước mảng nằm trong khoảng từ 2 ^ 16 đến 2 ^ 24, sử dụng tám vòng lặp. Tôi đã cẩn thận khởi tạo các mảng nguồn để việc +=gán không yêu cầu FPU thêm bộ nhớ rác được hiểu như một bộ đôi.

Tôi đã thử với nhiều kế hoạch khác nhau, chẳng hạn như đặt nhiệm vụ b[j], d[j]vào InitToZero[j]bên trong các vòng lặp, cũng như sử dụng += b[j] = 1+= d[j] = 1, và tôi đã nhận được kết quả khá nhất quán.

Như bạn có thể mong đợi, việc khởi tạo bdbên trong vòng lặp bằng cách sử dụng InitToZero[j]mang lại lợi thế cho phương pháp kết hợp, vì chúng được thực hiện liên tục trước khi thực hiện nhiệm vụ acnhưng vẫn nằm trong phạm vi 10%. Đi tìm hình.

Phần cứng là Dell XPS 8500 với Core i7 thế hệ 3 @ 3,4 GHz và bộ nhớ 8 GB. Đối với 2 ^ 16 đến 2 ^ 24, sử dụng tám vòng lặp, thời gian tích lũy lần lượt là 44,987 và 40,965. Visual C ++ 2010, được tối ưu hóa hoàn toàn.

Tái bút: Tôi đã thay đổi các vòng lặp để đếm ngược về 0, và phương pháp kết hợp nhanh hơn một chút. Gãi đầu. Lưu ý kích thước mảng mới và số lượng vòng lặp.

// 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;
}

Tôi không chắc tại sao lại quyết định rằng MFLOPS là một số liệu phù hợp. Mặc dù ý tưởng là tập trung vào truy cập bộ nhớ, vì vậy tôi đã cố gắng giảm thiểu thời gian tính toán dấu phẩy động. Tôi đã rời khỏi +=, nhưng tôi không chắc tại sao.

Một phép gán thẳng không tính toán sẽ là một bài kiểm tra rõ ràng hơn về thời gian truy cập bộ nhớ và sẽ tạo ra một bài kiểm tra đồng nhất bất kể số vòng lặp. Có thể tôi đã bỏ lỡ điều gì đó trong cuộc trò chuyện, nhưng điều đó đáng để suy nghĩ lại. Nếu điểm cộng bị bỏ qua khỏi bài tập, thời gian tích lũy gần như giống nhau ở mỗi điểm là 31 giây.

19
James 2011-12-18 10:52.

Đó là do CPU không có quá nhiều bộ nhớ cache (nơi nó phải đợi dữ liệu mảng đến từ các chip RAM). Sẽ rất thú vị nếu bạn điều chỉnh kích thước của các mảng liên tục để bạn vượt quá kích thước của bộ đệm cấp 1 (L1) và sau đó là bộ đệm cấp 2 (L2), của CPU và vẽ biểu đồ thời gian thực hiện cho mã của bạn để thực thi so với kích thước của mảng. Biểu đồ không nên là một đường thẳng như bạn mong đợi.

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

Vòng lặp đầu tiên xen kẽ việc viết trong mỗi biến. Cái thứ hai và thứ ba chỉ tạo ra những bước nhảy nhỏ về kích thước phần tử.

Thử viết hai đường thẳng song song gồm 20 chữ thập bằng bút và giấy cách nhau 20 cm. Hãy thử một lần hoàn thành dòng này rồi đến dòng kia và thử lần khác bằng cách viết xen kẽ một dấu thập ở mỗi dòng.

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

Câu hỏi ban đầu

Tại sao một vòng lặp lại chậm hơn nhiều so với hai vòng lặp?


Phần kết luận:

Trường hợp 1 là một bài toán nội suy cổ điển xảy ra là một bài toán không hiệu quả. Tôi cũng nghĩ rằng đây là một trong những lý do hàng đầu khiến nhiều nhà phát triển và kiến ​​trúc máy kết thúc việc xây dựng và thiết kế các hệ thống đa lõi với khả năng thực hiện các ứng dụng đa luồng cũng như lập trình song song.

Xem xét nó từ kiểu tiếp cận này mà không liên quan đến cách (các) Phần cứng, Hệ điều hành và Trình biên dịch hoạt động cùng nhau để thực hiện phân bổ đống liên quan đến việc làm việc với RAM, Cache, Tệp trang, v.v.; toán học là nền tảng của các thuật toán này cho chúng ta thấy cái nào trong hai cái này là giải pháp tốt hơn.

Chúng ta có thể sử dụng một phép tương tự về một Bosssinh Summationthể sẽ đại diện cho một sinh vật For Loopphải đi lại giữa các công nhân A& B.

We can easily see that Case 2 is at least half as fast if not a little more than Case 1 due to the difference in the distance that is needed to travel and the time taken between the workers. This math lines up almost virtually and perfectly with both the BenchMark Times as well as the number of differences in Assembly Instructions.


I will now begin to explain how all of this works below.


Assessing The Problem

The OP's code:

const int n=100000;

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

And

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

The Consideration

Considering the OP's original question about the 2 variants of the for loops and his amended question towards the behavior of caches along with many of the other excellent answers and useful comments; I'd like to try and do something different here by taking a different approach about this situation and problem.


The Approach

Considering the two loops and all of the discussion about cache and page filing I'd like to take another approach as to looking at this from a different perspective. One that doesn't involve the cache and page files nor the executions to allocate memory, in fact, this approach doesn't even concern the actual hardware or the software at all.


The Perspective

After looking at the code for a while it became quite apparent what the problem is and what is generating it. Let's break this down into an algorithmic problem and look at it from the perspective of using mathematical notations then apply an analogy to the math problems as well as to the algorithms.


What We Do Know

We know is that this loop will run 100,000 times. We also know that a1, b1, c1 & d1 are pointers on a 64-bit architecture. Within C++ on a 32-bit machine, all pointers are 4 bytes and on a 64-bit machine, they are 8 bytes in size since pointers are of a fixed length.

We know that we have 32 bytes in which to allocate for in both cases. The only difference is we are allocating 32 bytes or 2 sets of 2-8bytes on each iteration wherein the 2nd case we are allocating 16 bytes for each iteration for both of the independent loops.

Both loops still equal 32 bytes in total allocations. With this information let's now go ahead and show the general math, algorithms, and analogy of these concepts.

We do know the number of times that the same set or group of operations that will have to be performed in both cases. We do know the amount of memory that needs to be allocated in both cases. We can assess that the overall workload of the allocations between both cases will be approximately the same.


What We Don't Know

We do not know how long it will take for each case unless if we set a counter and run a benchmark test. However, the benchmarks were already included from the original question and from some of the answers and comments as well; and we can see a significant difference between the two and this is the whole reasoning for this proposal to this problem.


Let's Investigate

It is already apparent that many have already done this by looking at the heap allocations, benchmark tests, looking at RAM, Cache, and Page Files. Looking at specific data points and specific iteration indices were also included and the various conversations about this specific problem have many people starting to question other related things about it. How do we begin to look at this problem by using mathematical algorithms and applying an analogy to it? We start off by making a couple of assertions! Then we build out our algorithm from there.


Our Assertions:

  • We will let our loop and its iterations be a Summation that starts at 1 and ends at 100000 instead of starting with 0 as in the loops for we don't need to worry about the 0 indexing scheme of memory addressing since we are just interested in the algorithm itself.
  • In both cases we have 4 functions to work with and 2 function calls with 2 operations being done on each function call. We will set these up as functions and calls to functions as the following: F1(), F2(), f(a), f(b), f(c) and f(d).

The Algorithms:

1st Case: - Only one summation but two independent function calls.

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

2nd Case: - Two summations but each has its own function call.

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); }

If you noticed F2() only exists in Sum from Case1 where F1() is contained in Sum from Case1 and in both Sum1 and Sum2 from Case2. This will be evident later on when we begin to conclude that there is an optimization that is happening within the second algorithm.

The iterations through the first case Sum calls f(a) that will add to its self f(b) then it calls f(c) that will do the same but add f(d) to itself for each 100000 iterations. In the second case, we have Sum1 and Sum2 that both act the same as if they were the same function being called twice in a row.

In this case we can treat Sum1 and Sum2 as just plain old Sum where Sum in this case looks like this: Sum n=1 : [1,100000] { f(a) = f(a) + f(b); } and now this looks like an optimization where we can just consider it to be the same function.


Summary with Analogy

With what we have seen in the second case it almost appears as if there is optimization since both for loops have the same exact signature, but this isn't the real issue. The issue isn't the work that is being done by f(a), f(b), f(c), and f(d). In both cases and the comparison between the two, it is the difference in the distance that the Summation has to travel in each case that gives you the difference in execution time.

Think of the For Loops as being the Summations that does the iterations as being a Boss that is giving orders to two people A & B and that their jobs are to meat C & D respectively and to pick up some package from them and return it. In this analogy, the for loops or summation iterations and condition checks themselves don't actually represent the Boss. What actually represents the Boss is not from the actual mathematical algorithms directly but from the actual concept of Scope and Code Block within a routine or subroutine, method, function, translation unit, etc. The first algorithm has 1 scope where the 2nd algorithm has 2 consecutive scopes.

Within the first case on each call slip, the Boss goes to A and gives the order and A goes off to fetch B's package then the Boss goes to C and gives the orders to do the same and receive the package from D on each iteration.

Within the second case, the Boss works directly with A to go and fetch B's package until all packages are received. Then the Boss works with C to do the same for getting all of D's packages.

Since we are working with an 8-byte pointer and dealing with heap allocation let's consider the following problem. Let's say that the Boss is 100 feet from A and that A is 500 feet from C. We don't need to worry about how far the Boss is initially from C because of the order of executions. In both cases, the Boss initially travels from A first then to B. This analogy isn't to say that this distance is exact; it is just a useful test case scenario to show the workings of the algorithms.

In many cases when doing heap allocations and working with the cache and page files, these distances between address locations may not vary that much or they can vary significantly depending on the nature of the data types and the array sizes.


The Test Cases:

First Case: On first iteration the Boss has to initially go 100 feet to give the order slip to A and A goes off and does his thing, but then the Boss has to travel 500 feet to C to give him his order slip. Then on the next iteration and every other iteration after the Boss has to go back and forth 500 feet between the two.

Second Case: The Boss has to travel 100 feet on the first iteration to A, but after that, he is already there and just waits for A to get back until all slips are filled. Then the Boss has to travel 500 feet on the first iteration to C because C is 500 feet from A. Since this Boss( Summation, For Loop ) is being called right after working with A he then just waits there as he did with A until all of C's order slips are done.


The Difference In Distances Traveled

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;    

The Comparison of Arbitrary Values

We can easily see that 600 is far less than 10 million. Now, this isn't exact, because we don't know the actual difference in distance between which address of RAM or from which Cache or Page File each call on each iteration is going to be due to many other unseen variables. This is just an assessment of the situation to be aware of and looking at it from the worst-case scenario.

From these numbers it would almost appear as if Algorithm One should be 99% slower than Algorithm Two; however, this is only the Boss's part or responsibility of the algorithms and it doesn't account for the actual workers A, B, C, & D and what they have to do on each and every iteration of the Loop. So the boss's job only accounts for about 15 - 40% of the total work being done. The bulk of the work that is done through the workers has a slightly bigger impact towards keeping the ratio of the speed rate differences to about 50-70%


The Observation: - The differences between the two algorithms

In this situation, it is the structure of the process of the work being done. It goes to show that Case 2 is more efficient from both the partial optimization of having a similar function declaration and definition where it is only the variables that differ by name and the distance traveled.

We also see that the total distance traveled in Case 1 is much farther than it is in Case 2 and we can consider this distance traveled our Time Factor between the two algorithms. Case 1 has considerable more work to do than Case 2 does.

This is observable from the evidence of the ASM instructions that were shown in both cases. Along with what was already stated about these cases, this doesn't account for the fact that in Case 1 the boss will have to wait for both A & C to get back before he can go back to A again for each iteration. It also doesn't account for the fact that if A or B is taking an extremely long time then both the Boss and the other worker(s) are idle waiting to be executed.

In Case 2 the only one being idle is the Boss until the worker gets back. So even this has an impact on the algorithm.



The OPs Amended Question(s)

EDIT: The question turned out to be of no relevance, as the behavior severely depends on the sizes of the arrays (n) and the CPU cache. So if there is further interest, I rephrase the question:

Could you provide some solid insight into the details that lead to the different cache behaviors as illustrated by the five regions on the following graph?

It might also be interesting to point out the differences between CPU/cache architectures, by providing a similar graph for these CPUs.


Regarding These Questions

As I have demonstrated without a doubt, there is an underlying issue even before the Hardware and Software becomes involved.

Now as for the management of memory and caching along with page files, etc. which all work together in an integrated set of systems between the following:

  • The Architecture { Hardware, Firmware, some Embedded Drivers, Kernels and ASM Instruction Sets }.
  • The OS{ File and Memory Management systems, Drivers and the Registry }.
  • The Compiler { Translation Units and Optimizations of the Source Code }.
  • And even the Source Code itself with its set(s) of distinctive algorithms.

We can already see that there is a bottleneck that is happening within the first algorithm before we even apply it to any machine with any arbitrary Architecture, OS, and Programmable Language compared to the second algorithm. There already existed a problem before involving the intrinsics of a modern computer.


The Ending Results

However; it is not to say that these new questions are not of importance because they themselves are and they do play a role after all. They do impact the procedures and the overall performance and that is evident with the various graphs and assessments from many who have given their answer(s) and or comment(s).

If you paid attention to the analogy of the Boss and the two workers A & B who had to go and retrieve packages from C & D respectively and considering the mathematical notations of the two algorithms in question; you can see without the involvement of the computer hardware and software Case 2 is approximately 60% faster than Case 1.

When you look at the graphs and charts after these algorithms have been applied to some source code, compiled, optimized, and executed through the OS to perform their operations on a given piece of hardware, you can even see a little more degradation between the differences in these algorithms.

If the Data set is fairly small it may not seem all that bad of a difference at first. However, since Case 1 is about 60 - 70% slower than Case 2 we can look at the growth of this function in terms of the differences in time executions:

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)

This approximation is the average difference between these two loops both algorithmically and machine operations involving software optimizations and machine instructions.

When the data set grows linearly, so does the difference in time between the two. Algorithm 1 has more fetches than algorithm 2 which is evident when the Boss has to travel back and forth the maximum distance between A & C for every iteration after the first iteration while Algorithm 2 the Boss has to travel to A once and then after being done with A he has to travel a maximum distance only one time when going from A to C.

Trying to have the Boss focusing on doing two similar things at once and juggling them back and forth instead of focusing on similar consecutive tasks is going to make him quite angry by the end of the day since he had to travel and work twice as much. Therefore do not lose the scope of the situation by letting your boss getting into an interpolated bottleneck because the boss's spouse and children wouldn't appreciate it.



Amendment: Software Engineering Design Principles

-- The difference between Local Stack and Heap Allocated computations within iterative for loops and the difference between their usages, their efficiencies, and effectiveness --

The mathematical algorithm that I proposed above mainly applies to loops that perform operations on data that is allocated on the heap.

  • Consecutive Stack Operations:
    • If the loops are performing operations on data locally within a single code block or scope that is within the stack frame it will still sort of apply, but the memory locations are much closer where they are typically sequential and the difference in distance traveled or execution time is almost negligible. Since there are no allocations being done within the heap, the memory isn't scattered, and the memory isn't being fetched through ram. The memory is typically sequential and relative to the stack frame and stack pointer.
    • When consecutive operations are being done on the stack, a modern Processor will cache repetitive values and addresses keeping these values within local cache registers. The time of operations or instructions here is on the order of nano-seconds.
  • Consecutive Heap Allocated Operations:
    • When you begin to apply heap allocations and the processor has to fetch the memory addresses on consecutive calls, depending on the architecture of the CPU, the Bus Controller, and the Ram modules the time of operations or execution can be on the order of micro to milliseconds. In comparison to cached stack operations, these are quite slow.
    • The CPU will have to fetch the memory address from Ram and typically anything across the system bus is slow compared to the internal data paths or data buses within the CPU itself.

So when you are working with data that needs to be on the heap and you are traversing through them in loops, it is more efficient to keep each data set and its corresponding algorithms within its own single loop. You will get better optimizations compared to trying to factor out consecutive loops by putting multiple operations of different data sets that are on the heap into a single loop.

It is okay to do this with data that is on the stack since they are frequently cached, but not for data that has to have its memory address queried every iteration.

This is where Software Engineering and Software Architecture Design comes into play. It is the ability to know how to organize your data, knowing when to cache your data, knowing when to allocate your data on the heap, knowing how to design and implement your algorithms, and knowing when and where to call them.

You might have the same algorithm that pertains to the same data set, but you might want one implementation design for its stack variant and another for its heap-allocated variant just because of the above issue that is seen from its O(n) complexity of the algorithm when working with the heap.

From what I've noticed over the years many people do not take this fact into consideration. They will tend to design one algorithm that works on a particular data set and they will use it regardless of the data set being locally cached on the stack or if it was allocated on the heap.

If you want true optimization, yes it might seem like code duplication, but to generalize it would be more efficient to have two variants of the same algorithm. One for stack operations, and the other for heap operations that are performed in iterative loops!

Here's a pseudo example: Two simple structs, one algorithm.

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.

This is what I was referring to by having separate implementations for stack variants versus heap variants. The algorithms themselves don't matter too much, it's the looping structures that you will use them in that do.

2
mathengineer 2018-07-11 21:00.

It may be old C++ and optimizations. On my computer I obtained almost the same speed:

One loop: 1.577 ms

Two loops: 1.507 ms

I run Visual Studio 2015 on an E5-1620 3.5 GHz processor with 16 GB RAM.

Related questions

MORE COOL STUFF

Cate Blanchett chia tay chồng sau 3 ngày bên nhau và vẫn kết hôn với anh ấy 25 năm sau

Cate Blanchett chia tay chồng sau 3 ngày bên nhau và vẫn kết hôn với anh ấy 25 năm sau

Cate Blanchett đã bất chấp những lời khuyên hẹn hò điển hình khi cô gặp chồng mình.

Tại sao Michael Sheen là một diễn viên phi lợi nhuận

Tại sao Michael Sheen là một diễn viên phi lợi nhuận

Michael Sheen là một diễn viên phi lợi nhuận nhưng chính xác thì điều đó có nghĩa là gì?

Hallmark Star Colin Egglesfield Các món ăn gây xúc động mạnh đối với người hâm mộ tại RomaDrama Live! [Loại trừ]

Hallmark Star Colin Egglesfield Các món ăn gây xúc động mạnh đối với người hâm mộ tại RomaDrama Live! [Loại trừ]

Ngôi sao của Hallmark Colin Egglesfield chia sẻ về những cuộc gặp gỡ với người hâm mộ ly kỳ tại RomaDrama Live! cộng với chương trình INSPIRE của anh ấy tại đại hội.

Tại sao bạn không thể phát trực tuyến 'chương trình truyền hình phía Bắc'

Tại sao bạn không thể phát trực tuyến 'chương trình truyền hình phía Bắc'

Bạn sẽ phải phủi sạch đầu đĩa Blu-ray hoặc DVD để xem tại sao Northern Exposure trở thành một trong những chương trình nổi tiếng nhất của thập niên 90.

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!

8 công dụng tuyệt vời của Baking Soda và Giấm

8 công dụng tuyệt vời của Baking Soda và Giấm

Bạn biết đấy, hai sản phẩm này là nguồn điện để làm sạch, riêng chúng. Nhưng cùng với nhau, chúng có một loạt công dụng hoàn toàn khác.

Hạn hán, biến đổi khí hậu đe dọa tương lai của thủy điện Hoa Kỳ

Hạn hán, biến đổi khí hậu đe dọa tương lai của thủy điện Hoa Kỳ

Thủy điện rất cần thiết cho lưới điện của Hoa Kỳ, nhưng nó chỉ tạo ra năng lượng khi có nước di chuyển. Bao nhiêu nhà máy thủy điện có thể gặp nguy hiểm khi các hồ và sông cạn kiệt?

Quyên góp tóc của bạn để giúp giữ nước sạch của chúng tôi

Quyên góp tóc của bạn để giúp giữ nước sạch của chúng tôi

Tóc tỉa từ các tiệm và các khoản quyên góp cá nhân có thể được tái sử dụng như những tấm thảm thấm dầu và giúp bảo vệ môi trường.

Xem đoạn giới thiệu cho bộ phim chuyển thể đầy khói lửa, có sự góp mặt của ngôi sao Edward Norton từ phim Motherless Brooklyn của Jonathan Lethem

Xem đoạn giới thiệu cho bộ phim chuyển thể đầy khói lửa, có sự góp mặt của ngôi sao Edward Norton từ phim Motherless Brooklyn của Jonathan Lethem

Edward Norton đã muốn đưa cuốn tiểu thuyết Motherless Brooklyn của Jonathan Lethem's Joycean 1999 lên màn ảnh kể từ khi nó được xuất bản. Bây giờ, 20 năm sau, một đoạn giới thiệu đã xuất hiện cho câu chuyện sôi nổi, đưa câu chuyện của Lethem trở lại những năm 1950 với rất nhiều gương mặt quen thuộc.

Cách dễ dàng chọn không tham gia trọng tài ràng buộc thẻ Apple

Cách dễ dàng chọn không tham gia trọng tài ràng buộc thẻ Apple

Có thích thú khi sử dụng Thẻ Apple mới của bạn không? Trước khi bạn bắt đầu chi tiêu, có một nhiệm vụ bổ sung cần xem xét: chọn không tham gia trọng tài ràng buộc. Bạn sẽ phát hiện ra các điều khoản trọng tài ràng buộc trong nhiều thỏa thuận tài chính vì nó giúp ngăn các ngân hàng và đối tác kinh doanh của họ không phải ra tòa.

Những người chơi Fortnite World Cup không ghi bàn có cảm giác hài hước về điều đó

Những người chơi Fortnite World Cup không ghi bàn có cảm giác hài hước về điều đó

Điểm số tại vòng chung kết Fortnite World Cup Solo hôm nay là rất lớn, với người chiến thắng Bugha ghi được nhiều hơn 26 điểm so với người về thứ hai là Psalm. Nhưng không phải ai cũng có thể giành chiến thắng: Bốn cầu thủ ra về với 0 điểm, nhưng — ít nhất là trên Twitter — họ là những người thể thao tốt về điều đó.

Báo cáo: Cánh cửa tuyển sinh có thể đã mở cho các ứng viên UCLA có mối quan hệ có ảnh hưởng

Báo cáo: Cánh cửa tuyển sinh có thể đã mở cho các ứng viên UCLA có mối quan hệ có ảnh hưởng

Huấn luyện viên trưởng của bộ môn thể dục dụng cụ UCLA Valorie Kondos-Field theo dõi Katelyn Ohashi thi đấu thăng bằng trong trận gặp Stanford tại Pauley Pavilion vào ngày 10 tháng 3 năm 2019 ở Los Angeles, California. Vụ bê bối gian lận tuyển sinh đại học tiết lộ các chi tiết của một quá trình chính thức hóa để đưa những đứa trẻ thất bại của các gia đình giàu có và nổi tiếng vào các trường đại học đáng tin cậy và danh tiếng, sử dụng một "cửa phụ" đắt tiền cho các bậc cha mẹ mà sự giàu có của họ khiến họ không có cơ hội. chỉ cần tài trợ cho một cánh mới trong khuôn viên trường.

Nicky Hilton Forced to Borrow Paris' 'I Love Paris' Sweatshirt After 'Airline Loses All [My] Luggage'

Nicky Hilton Forced to Borrow Paris' 'I Love Paris' Sweatshirt After 'Airline Loses All [My] Luggage'

Nicky Hilton Rothschild's luggage got lost, but luckily she has an incredible closet to shop: Sister Paris Hilton's!

Kate Middleton dành một ngày bên bờ nước ở London, cùng với Jennifer Lopez, Julianne Hough và hơn thế nữa

Kate Middleton dành một ngày bên bờ nước ở London, cùng với Jennifer Lopez, Julianne Hough và hơn thế nữa

Kate Middleton dành một ngày bên bờ nước ở London, cùng với Jennifer Lopez, Julianne Hough và hơn thế nữa. Từ Hollywood đến New York và mọi nơi ở giữa, hãy xem các ngôi sao yêu thích của bạn đang làm gì!

17 tuổi bị đâm chết trong khi 4 người khác bị thương trong một cuộc tấn công bằng dao trên sông Wisconsin

17 tuổi bị đâm chết trong khi 4 người khác bị thương trong một cuộc tấn công bằng dao trên sông Wisconsin

Các nhà điều tra đang xem xét liệu nhóm và nghi phạm có biết nhau trước vụ tấn công hay không

Thanh thiếu niên, Gia đình Florida Hội đồng quản trị trường học về Luật 'Không nói đồng tính': 'Buộc chúng tôi tự kiểm duyệt'

Thanh thiếu niên, Gia đình Florida Hội đồng quản trị trường học về Luật 'Không nói đồng tính': 'Buộc chúng tôi tự kiểm duyệt'

Vụ kiện, nêu tên một số học khu, lập luận rằng dự luật "Không nói đồng tính" được ban hành gần đây của Florida "có hiệu quả im lặng và xóa bỏ học sinh và gia đình LGBTQ +"

Hãy tưởng tượng tạo ra một chiến lược nội dung thực sự CHUYỂN ĐỔI. Nó có thể.

Hãy tưởng tượng tạo ra một chiến lược nội dung thực sự CHUYỂN ĐỔI. Nó có thể.

Vào năm 2021, tôi khuyến khích bạn suy nghĩ lại mọi thứ bạn biết về khách hàng mà bạn phục vụ và những câu chuyện bạn kể cho họ. Lùi lại.

Sự mất mát của voi ma mút đã mở ra trái tim tôi để yêu

Sự mất mát của voi ma mút đã mở ra trái tim tôi để yêu

Vào ngày sinh nhật thứ 9 của Felix The Cat, tôi nhớ về một trong những mất mát lớn nhất trong cuộc đời trưởng thành của tôi - Sophie của tôi vào năm 2013. Tôi đã viết bài luận này và chia sẻ nó trên nền tảng này một thời gian ngắn vào năm 2013.

Khi bạn không thể trở thành người mà Internet muốn bạn trở thành

Khi bạn không thể trở thành người mà Internet muốn bạn trở thành

Tôi ghét từ "tàu đắm". Mọi người cảm thấy thoải mái trong la bàn đạo đức của riêng mình, và khi làm như vậy, họ thấy mình vượt qua sự phán xét.

Tầm nhìn đám mây phi tập trung của DFINITY Blockchain

Lưu ý của người biên tập: Bạn đang xem tài liệu lỗi thời từ blog DFINITY đang được bảo quản cho mục đích lưu trữ.

Tầm nhìn đám mây phi tập trung của DFINITY Blockchain

Bài đăng này khám phá tầm nhìn về đám mây phi tập trung của nhóm DFINITY và cách nó liên quan đến các nhà cung cấp blockchain truyền thống và đám mây hiện có như Amazon Web Services. Các minh chứng về công nghệ DFINITY được áp dụng bởi một mạng lưới quy mô lớn sẽ được thực hiện vào mùa thu năm 2017, sau đó sẽ được gây quỹ Chính cho quỹ hỗ trợ phi lợi nhuận, với mạng “đám mây mở” dự kiến ​​sẽ ra mắt vào đầu mùa hè năm 2018 .

Language