Pertanyaan Mengapa penambahan elemen lebih cepat di loop terpisah daripada di loop gabungan?


Seharusnya a1, b1, c1, dan d1 arahkan ke memori heap dan kode numerik saya memiliki loop inti berikut.

const int n = 100000;

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

Lingkaran ini dieksekusi 10.000 kali melalui luar yang lain for lingkaran. Untuk mempercepatnya, saya mengubah kode ke:

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

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

Disusun pada MS Visual C ++ 10.0 dengan optimalisasi penuh dan SSE2 diaktifkan untuk 32-bit pada Intel Core 2 Duo (x64), contoh pertama membutuhkan 5,5 detik dan contoh loop ganda hanya membutuhkan 1,9 detik. Pertanyaan saya adalah: (Silakan merujuk ke pertanyaan saya yang diulang di bagian bawah)

PS: Saya tidak yakin, apakah ini membantu:

Disassembly untuk loop pertama pada dasarnya terlihat seperti ini (blok ini diulang sekitar lima kali dalam program penuh):

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]

Setiap loop dari contoh loop ganda menghasilkan kode ini (blok berikut diulang sekitar tiga kali):

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

Pertanyaannya ternyata tidak ada relevansinya, karena perilaku sangat bergantung pada ukuran array (n) dan cache CPU. Jadi jika ada minat lebih lanjut, saya ulangi pertanyaannya:

Bisakah Anda memberikan beberapa wawasan yang kuat ke dalam rincian yang mengarah pada perilaku cache yang berbeda seperti yang digambarkan oleh lima wilayah pada grafik berikut?

Mungkin juga menarik untuk menunjukkan perbedaan antara arsitektur CPU / cache, dengan menyediakan grafik serupa untuk CPU ini.

PPS: Ini kode lengkapnya. Itu menggunakan TBB  Tick_Count untuk waktu resolusi yang lebih tinggi, yang dapat dinonaktifkan dengan tidak mendefinisikan TBB_TIMING Makro:

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

(Ini menunjukkan FLOP / s untuk nilai yang berbeda n.)

enter image description here


1995
2017-12-17 20:40


asal


Jawaban:


Setelah analisis lebih lanjut tentang ini, saya yakin ini (setidaknya sebagian) disebabkan oleh penyelarasan data dari empat pointer. Ini akan menyebabkan beberapa tingkat konflik cache bank / cara.

Jika saya sudah menebak dengan benar tentang bagaimana Anda mengalokasikan array Anda, mereka cenderung sejajar dengan garis halaman.

Ini berarti bahwa semua akses Anda di setiap loop akan jatuh pada cara cache yang sama. Namun, prosesor Intel memiliki 8-way L1 cache associativity untuk sementara waktu. Namun dalam kenyataannya, kinerjanya tidak sepenuhnya seragam. Mengakses 4-cara masih lebih lambat daripada mengatakan 2-cara.

EDIT: Itu memang terlihat seperti Anda mengalokasikan semua array secara terpisah. Biasanya ketika alokasi besar diminta, pengalokasi akan meminta halaman baru dari OS. Oleh karena itu, ada kemungkinan besar alokasi besar akan muncul pada offset yang sama dari batas halaman.

Ini kode tesnya:

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

Hasil Benchmark:

EDIT: Hasil pada sebenarnya Mesin arsitektur 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

Observasi:

  • 6,206 detik dengan satu lingkaran dan 2,116 detik dengan dua putaran. Ini mereproduksi hasil OP tepat.

  • Dalam dua tes pertama, array dialokasikan secara terpisah.Anda akan melihat bahwa semuanya memiliki kesejajaran yang sama dengan halaman.

  • Dalam dua tes kedua, array tersebut dikemas bersama untuk mematahkan penyelarasan itu. Di sini Anda akan melihat kedua loop lebih cepat. Selanjutnya, loop kedua (double) sekarang adalah yang lebih lambat seperti yang biasanya Anda harapkan.

Seperti yang disebutkan @Stephen Cannon dalam komentar, ada kemungkinan besar kemungkinan penyebab keselarasan ini aliasing palsu di unit pemuatan / penyimpanan atau cache. Saya mencari-cari Google untuk ini dan menemukan bahwa Intel benar-benar memiliki counter perangkat keras untuk aliasing alamat parsial warung:

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


5 Wilayah - Penjelasan

Wilayah 1:

Yang ini mudah. Dataset ini sangat kecil sehingga kinerjanya didominasi oleh overhead seperti perulangan dan percabangan.

Wilayah 2:

Di sini, ketika ukuran data meningkat, jumlah overhead relatif turun dan kinerja "jenuh". Di sini dua loop lebih lambat karena memiliki loop dua kali lebih banyak dan bercabang di atas.

Saya tidak yakin apa yang terjadi di sini ... Alignment masih bisa memainkan efek seperti yang disebutkan Agner Fog konflik cache bank. (Tautan itu adalah tentang Sandy Bridge, tetapi gagasan itu harus tetap berlaku untuk Core 2.)

Wilayah 3:

Pada titik ini, data tidak lagi cocok di cache L1. Jadi kinerja dibatasi oleh L1 <-> L2 cache bandwidth.

Wilayah 4:

Penurunan kinerja dalam loop tunggal adalah apa yang kita amati. Dan seperti yang disebutkan, ini disebabkan oleh penyelarasan yang (kemungkinan besar) penyebab aliasing palsu kios di unit pemuatan / toko beban.

Namun, agar aliasing palsu terjadi, harus ada langkah yang cukup besar antara kumpulan data. Inilah mengapa Anda tidak melihat ini di wilayah 3.

Wilayah 5:

Pada titik ini, tidak ada yang cocok dalam cache. Jadi Anda terikat oleh bandwidth memori.


2 x Intel X5482 Harpertown @ 3.2 GHz Intel Core i7 870 @ 2.8 GHz Intel Core i7 2600K @ 4.4 GHz


1544
2017-12-17 21:17



Oke, jawaban yang benar pasti harus melakukan sesuatu dengan cache CPU. Tetapi untuk menggunakan argumen cache bisa sangat sulit, terutama tanpa data.

Ada banyak jawaban, yang mengarah ke banyak diskusi, tetapi mari kita hadapi: Masalah cache bisa sangat rumit dan tidak satu dimensi. Mereka sangat bergantung pada ukuran data, jadi pertanyaan saya tidak adil: Ternyata pada titik yang sangat menarik dalam grafik cache.

@ Jawaban Mysticial meyakinkan banyak orang (termasuk saya), mungkin karena itu adalah satu-satunya yang tampaknya bergantung pada fakta, tetapi itu hanya satu "titik data" dari kebenaran.

Itulah mengapa saya menggabungkan tesnya (menggunakan alokasi kontinu vs. terpisah) dan saran @James 'Penjawab.

Grafik di bawah ini menunjukkan, bahwa sebagian besar jawaban dan terutama sebagian besar komentar untuk pertanyaan dan jawaban dapat dianggap sepenuhnya salah atau benar tergantung pada skenario dan parameter yang digunakan.

Perhatikan bahwa pertanyaan awal saya adalah pada n = 100.000. Titik ini (secara tidak sengaja) menunjukkan perilaku khusus:

  1. Ini memiliki perbedaan terbesar antara satu dan dua versi loop'ed (hampir tiga faktor)

  2. Ini adalah satu-satunya titik, di mana satu-loop (yaitu dengan alokasi berkelanjutan) mengalahkan versi dua-loop. (Ini membuat jawaban Mysticial mungkin, sama sekali.)

Hasilnya menggunakan data yang diinisialisasi:

Enter image description here

Hasilnya menggunakan data terinisialisasi (ini adalah apa yang diuji Mysticial):

Enter image description here

Dan ini adalah yang sulit dijelaskan: Data yang diinisialisasi, yang dialokasikan sekali dan digunakan kembali untuk setiap kasus pengujian berikut dengan ukuran vektor yang berbeda:

Enter image description here

Usul

Setiap pertanyaan terkait kinerja tingkat rendah di Stack Overflow harus diminta untuk memberikan informasi MFLOPS untuk seluruh rentang ukuran data cache yang relevan! Ini membuang-buang waktu semua orang untuk memikirkan jawaban dan terutama mendiskusikannya dengan orang lain tanpa informasi ini.


194
2017-12-18 01:29



Putaran kedua melibatkan lebih sedikit aktivitas cache, jadi lebih mudah bagi prosesor untuk memenuhi tuntutan memori.


63
2017-12-17 20:47



Bayangkan Anda sedang mengerjakan sebuah mesin di mana n hanya nilai yang tepat untuk itu hanya mungkin untuk menahan dua dari array Anda dalam memori pada satu waktu, tetapi total memori yang tersedia, melalui disk caching, masih cukup untuk menampung keempatnya.

Dengan asumsi kebijakan cache LIFO sederhana, kode ini:

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

pertama akan menyebabkan a dan b untuk dimuat ke RAM dan kemudian dikerjakan seluruhnya dalam RAM. Ketika loop kedua dimulai, c dan d kemudian akan dimuat dari disk ke RAM dan dioperasikan.

loop lainnya

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

akan halaman dua array dan halaman di dua lainnya setiap kali di sekitar loop. Ini jelas akan terjadi banyak lebih lambat.

Anda mungkin tidak melihat disk cache dalam tes Anda tetapi Anda mungkin melihat efek samping dari beberapa bentuk lain dari caching.


Sepertinya ada sedikit kebingungan / kesalahpahaman di sini jadi saya akan mencoba menguraikan sedikit menggunakan contoh.

Mengatakan n = 2 dan kami bekerja dengan byte. Dalam skenario saya, kami memiliki demikian hanya 4 byte cache dan sisa memori kami secara signifikan lebih lambat (katakanlah 100 kali lebih lama akses).

Dengan asumsi kebijakan caching yang cukup bodoh jika byte tidak dalam cache, taruh di sana dan dapatkan byte berikut juga ketika kita berada di dalamnya Anda akan mendapatkan skenario seperti ini:

  • Dengan

    for(int j=0;j<n;j++){
     a[j] += b[j];
    }
    for(int j=0;j<n;j++){
     c[j] += d[j];
    }
    
  • cache a[0] dan a[1] kemudian b[0] dan b[1] dan mengatur a[0] = a[0] + b[0] dalam cache - sekarang ada empat byte dalam cache, a[0], a[1] dan b[0], b[1]. Biaya = 100 + 100.

  • set a[1] = a[1] + b[1] dalam cache. Biaya = 1 + 1.
  • Ulangi untuk c dan d.
  • Biaya total = (100 + 100 + 1 + 1) * 2 = 404

  • Dengan

    for(int j=0;j<n;j++){
     a[j] += b[j];
     c[j] += d[j];
    }
    
  • cache a[0] dan a[1] kemudian b[0] dan b[1] dan mengatur a[0] = a[0] + b[0] dalam cache - sekarang ada empat byte dalam cache, a[0], a[1] dan b[0], b[1]. Biaya = 100 + 100.

  • mengeluarkan a[0], a[1], b[0], b[1] dari cache dan cache c[0] dan c[1] kemudian d[0] dan d[1] dan mengatur c[0] = c[0] + d[0] dalam cache. Biaya = 100 + 100.
  • Saya kira Anda mulai melihat ke mana saya pergi.
  • Biaya total = (100 + 100 + 100 + 100) * 2 = 800

Ini adalah skenario thrash cache klasik.


37
2017-12-18 01:36



Ini bukan karena kode yang berbeda, tetapi karena cache: RAM lebih lambat daripada register CPU dan memori cache berada di dalam CPU untuk menghindari menulis RAM setiap kali variabel berubah. Tetapi cache tidak sebesar RAM, karenanya, ia hanya memetakan sebagian kecil dari itu.

Kode pertama memodifikasi alamat memori jauh yang bergantian mereka di setiap loop, sehingga membutuhkan terus menerus untuk membatalkan cache.

Kode kedua tidak bergantian: itu hanya mengalir pada alamat yang berdekatan dua kali. Ini membuat semua pekerjaan harus diselesaikan dalam cache, membatalkannya hanya setelah loop kedua dimulai.


27
2017-12-17 20:49



Saya tidak dapat mereplikasi hasil yang dibahas di sini.

Saya tidak tahu apakah kode patokan yang buruk harus disalahkan, atau apa, tetapi dua metode itu masing-masing berjarak 10% dari satu sama lain di mesin saya menggunakan kode berikut, dan satu putaran biasanya hanya sedikit lebih cepat daripada dua - seperti Anda mengharapkan.

Ukuran array berkisar dari 2 ^ 16 hingga 2 ^ 24, menggunakan delapan loop. Saya berhati - hati untuk menginisialisasi array sumber jadi += tugas tidak menanyakan FPU untuk menambahkan sampah memori ditafsirkan sebagai ganda.

Saya bermain-main dengan berbagai skema, seperti menempatkan tugas b[j], d[j] untuk InitToZero[j] di dalam loop, dan juga dengan menggunakan += b[j] = 1 dan += d[j] = 1, dan saya mendapat hasil yang cukup konsisten.

Seperti yang Anda duga, melakukan inisialisasi b dan d di dalam loop menggunakan InitToZero[j] memberikan pendekatan gabungan suatu keuntungan, karena mereka melakukan back-to-back sebelum penugasan a dan c, tetapi masih dalam 10%. Go figure.

Hardware adalah Dell XPS 8500 dengan generasi 3 Core i7 @ 3,4 GHz dan memori 8 GB. Untuk 2 ^ 16 hingga 2 ^ 24, menggunakan delapan loop, waktu kumulatif adalah 44.987 dan 40.965 masing-masing. Visual C ++ 2010, sepenuhnya dioptimalkan.

PS: Saya mengubah loop untuk menghitung mundur ke nol, dan metode gabungan sedikit lebih cepat. Menggaruk kepalaku. Perhatikan ukuran sizing dan loop array baru.

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

Saya tidak yakin mengapa diputuskan bahwa MFLOPS adalah metrik yang relevan. Saya pikir idenya adalah fokus pada akses memori, jadi saya mencoba untuk meminimalkan jumlah waktu perhitungan floating point. Saya meninggalkan di +=, tetapi saya tidak yakin mengapa.

Penetapan langsung tanpa perhitungan akan menjadi tes yang lebih bersih dari waktu akses memori dan akan membuat tes yang seragam terlepas dari jumlah loop. Mungkin saya melewatkan sesuatu dalam percakapan, tetapi ada baiknya berpikir dua kali. Jika nilai plus yang tersisa dari tugas, waktu kumulatif hampir identik pada 31 detik masing-masing.


16
2017-12-30 01:34



Itu karena CPU tidak memiliki begitu banyak cache misses (di mana ia harus menunggu data array berasal dari chip RAM). Akan menarik bagi Anda untuk menyesuaikan ukuran array terus sehingga Anda melebihi ukuran cache level 1 (L1), dan kemudian tingkat 2 cache (L2), dari CPU Anda dan plot waktu yang dibutuhkan untuk mengeksekusi kode Anda terhadap ukuran array. Grafik tidak boleh berupa garis lurus seperti yang Anda harapkan.


14
2017-12-17 20:52



Lingkaran pertama bergantian menulis di setiap variabel. Yang kedua dan ketiga hanya membuat lompatan kecil dari ukuran elemen.

Cobalah menulis dua garis sejajar dari 20 tanda silang dengan pena dan kertas yang dipisahkan oleh 20 cm. Cobalah sekali menyelesaikan satu dan kemudian garis yang lain dan coba lain waktu dengan membuat tanda silang di setiap baris secara bergantian.


12
2017-08-17 15:23