Halo!
Pada artikel sebelumnya , kami menggunakan cache prosesor untuk mempercepat grafik pada mikrokontroler di Embox . Kami menggunakan mode "tulis-tayang". Kemudian kami menulis tentang beberapa keuntungan dan kerugian yang terkait dengan mode "tulis-tayang", tetapi ini hanya gambaran umum sepintas. Pada artikel ini, seperti yang dijanjikan, saya ingin melihat lebih dekat jenis cache di mikrokontroler ARM, serta membandingkannya. Tentu saja, semua ini akan dipertimbangkan dari sudut pandang seorang programmer, dan kami tidak berencana untuk membahas detail pengontrol memori dalam artikel ini.
Saya akan mulai dengan bagian saya berhenti di artikel sebelumnya, yaitu perbedaan antara mode "tulis kembali" dan "tulis kembali", karena kedua mode ini paling sering digunakan. Pendeknya:
- "Menulis kembali". Menulis data hanya masuk ke cache. Penulisan aktual ke memori ditunda hingga cache penuh dan diperlukan ruang untuk data baru.
- "Menulis melalui". Penulisan terjadi "secara bersamaan" ke cache dan memori.
Menulis melalui
Manfaat menulis-tayang dianggap kemudahan penggunaan, yang berpotensi mengurangi kesalahan. Memang, dalam mode ini memori selalu dalam keadaan yang benar dan tidak memerlukan prosedur pembaruan tambahan.
Tentu saja, sepertinya ini berdampak besar pada kinerja, tetapi STM itu sendiri dalam dokumen ini mengatakan bahwa itu bukan:
Write-through: triggers a write to the memory as soon as the contents on the cache line are written to. This is safer for the data coherency, but it requires more bus accesses. In practice, the write to the memory is done in the background and has a little effect unless the same cache set is being accessed repeatedly and very quickly. It is always a tradeoff.Artinya, awalnya kami berasumsi bahwa karena penulisan adalah ke memori, maka kinerja pada operasi tulis akan hampir sama dengan tanpa cache sama sekali, dan keuntungan utama terjadi karena pembacaan berulang. Namun, STM membantahnya, ia mengatakan bahwa data dalam memori berada "di latar belakang", sehingga kinerja tulis hampir sama seperti dalam mode "tulis kembali". Ini, khususnya, mungkin bergantung pada buffer internal pengontrol memori (FMC).
Kekurangan dari mode "tulis-tayang":
- Akses berurutan dan cepat ke memori yang sama dapat menurunkan kinerja. Dalam mode "tulis kembali", akses frekuensi berurutan ke memori yang sama, sebaliknya, akan menjadi nilai tambah.
- Seperti dalam kasus "write-back", Anda masih perlu melakukan cache tidak valid setelah operasi DMA berakhir.
- Bug “Kerusakan data dalam urutan penyimpanan dan pemuatan Write-Through” di beberapa versi Cortex-M7. Hal itu ditunjukkan kepada kami oleh salah satu pengembang LVGL.
Menulis kembali
Seperti disebutkan di atas, dalam mode ini (sebagai lawan dari "tulis-melalui") data umumnya tidak masuk ke memori dengan menulis, tetapi hanya masuk ke cache. Seperti tulis-tayang, strategi ini memiliki dua sub-opsi - 1) alokasi tulis, 2) tidak ada alokasi tulis. Kami akan membicarakan opsi ini lebih lanjut.
Tulis Alokasikan
Sebagai aturan, "alokasi baca" selalu digunakan dalam cache - yaitu, jika cache tidak dibaca, data diambil dari memori dan ditempatkan di cache. Demikian pula, kesalahan penulisan dapat menyebabkan data dimuat ke cache ("alokasi tulis") atau tidak dimuat ("tidak ada alokasi tulis").
Biasanya, dalam praktiknya, kombinasi "tulis kembali alokasi tulis" atau "tulis-tayang tanpa alokasi tulis" digunakan. Lebih lanjut dalam pengujian kami akan mencoba untuk memeriksa secara lebih rinci dalam situasi apa menggunakan "alokasi tulis", dan di mana "tidak ada alokasi tulis".
MPU
Sebelum beralih ke bagian praktis, kita perlu mencari cara untuk mengatur parameter wilayah memori. Untuk memilih mode cache (atau menonaktifkannya) untuk wilayah memori tertentu dalam arsitektur ARMv7-M, MPU (Memory Protection Unit) digunakan.
Pengontrol MPU mendukung pengaturan wilayah memori. Secara khusus, dalam arsitektur ARMV7-M, bisa ada hingga 16 wilayah. Untuk wilayah ini, Anda dapat menyetel secara independen: alamat awal, ukuran, hak akses (baca / tulis / eksekusi, dll.), Atribut - TEX, dapat di-cache, dapat disangga, dapat dibagikan, serta parameter lainnya. Dengan mekanisme ini, khususnya, Anda dapat mencapai semua jenis caching untuk wilayah tertentu. Sebagai contoh, kita dapat menghilangkan kebutuhan untuk memanggil cache_clean / cache_invalidate dengan hanya mengalokasikan wilayah memori untuk semua operasi DMA dan menandai memori tersebut sebagai tidak dapat disimpan dalam cache.
Poin penting yang perlu diperhatikan saat bekerja dengan MPU:
Alamat dasar, ukuran, dan atribut suatu kawasan semuanya dapat dikonfigurasi, dengan aturan umum bahwa semua kawasan selaras secara alami. Ini dapat dinyatakan sebagai:Dengan kata lain, alamat awal wilayah memori harus disesuaikan dengan ukurannya sendiri. Jika Anda memiliki, misalnya, wilayah 16 KB, maka Anda harus menyelaraskannya dengan 16 KB. Jika wilayah memori adalah 64 Kb, maka sejajarkan dengan 64 Kb. Dan seterusnya. Jika ini tidak dilakukan, MPU dapat secara otomatis “memotong” wilayah tersebut ke ukuran yang sesuai dengan alamat awalnya (diuji dalam praktik).
RegionBaseAddress [(N-1): 0] = 0, di mana N adalah log2 (SizeofRegion_in_bytes)
Ngomong-ngomong, ada beberapa bug di STM32Cube. Contohnya:
MPU_InitStruct.BaseAddress = 0x20010000;
MPU_InitStruct.Size = MPU_REGION_SIZE_256KB;
Anda dapat melihat bahwa alamat awal adalah 64 KB selaras. Dan kami ingin ukuran wilayahnya menjadi 256 KB. Dalam hal ini, Anda harus membuat 3 wilayah: 64 Kb pertama, 128 Kb kedua, dan 64 Kb ketiga.
Anda hanya perlu menentukan kawasan yang berbeda dari properti standar. Faktanya adalah bahwa atribut dari semua memori ketika cache prosesor diaktifkan dijelaskan dalam arsitektur ARM. Ada sekumpulan properti standar (misalnya, inilah mengapa STM32F7 SRAM memiliki mode "tulis-kembali tuliskan" secara default). Oleh karena itu, jika Anda memerlukan mode non-standar untuk beberapa memori, Anda perlu menyetel propertinya melalui MPU. Dalam hal ini, di dalam wilayah, Anda dapat menyetel sub-wilayah dengan propertinya sendiri, Memilih di dalam wilayah ini satu sama lain dengan prioritas tinggi dengan properti yang diperlukan.
TCM
Sebagai berikut dari dokumentasi (bagian 2.3 SRAM Tertanam), 64 KB pertama SRAM di STM32F7 tidak dapat disimpan dalam cache. Dalam arsitektur ARMv7-M itu sendiri, SRAM terletak di 0x20000000. TCM juga mengacu pada SRAM, tetapi berada pada bus yang berbeda relatif terhadap sisa memori (SRAM1 dan SRAM2), dan terletak "lebih dekat" ke prosesor. Karena itu, memori ini sangat cepat, bahkan memiliki kecepatan yang sama dengan cache. Dan karena itu, cache tidak diperlukan, dan wilayah ini tidak dapat dibuat cache. Faktanya, TCM adalah jenis cache lain.
Cache instruksi
Perlu dicatat bahwa semua yang dibahas di atas mengacu pada cache data (D-Cache). Tetapi selain cache data, ARMv7-M juga menyediakan cache instruksi - Cache instruksi (I-Cache). I-Cache memungkinkan Anda untuk mentransfer beberapa instruksi yang dapat dieksekusi (dan selanjutnya) ke cache, yang secara signifikan dapat mempercepat program. Terutama dalam kasus di mana kode berada dalam memori yang lebih lambat daripada FLASH, misalnya, QSPI.
Untuk mengurangi ketidakpastian dalam pengujian dengan cache di bawah ini, kami akan menonaktifkan I-Cache secara sengaja dan memikirkan data secara eksklusif.
Pada saat yang sama, saya ingin mencatat bahwa mengaktifkan I-Cache cukup sederhana dan tidak memerlukan tindakan tambahan apa pun dari MPU, tidak seperti D-Cache.
Tes sintetis
Setelah membahas bagian teoritis, mari beralih ke tes untuk lebih memahami perbedaan dan ruang lingkup penerapan model tertentu. Seperti yang saya katakan di atas, nonaktifkan I-Cache dan hanya berfungsi dengan D-Cache. Saya juga sengaja mengkompilasi dengan -O0 sehingga loop dalam pengujian tidak dioptimalkan. Kami akan menguji melalui memori SDRAM eksternal. Dengan bantuan MPU, saya menandai wilayah 64 KB, dan kami akan mengekspos atribut yang kami butuhkan ke wilayah ini.
Karena pengujian dengan cache sangat berubah-ubah dan dipengaruhi oleh segala hal dan semua orang di sistem - mari kita buat kodenya linier dan berkelanjutan. Untuk melakukan ini, nonaktifkan interupsi. Selain itu, kami akan mengukur waktu bukan dengan pengatur waktu, tetapi dengan DWT (Data Watchpoint and Trace unit), yang memiliki penghitung siklus prosesor 32-bit. Atas dasar (di Internet) orang membuat penundaan mikrodetik pada pengemudi. Penghitung cepat meluap pada frekuensi sistem 216 MHz, tetapi Anda dapat mengukur hingga 20 detik. Kami hanya akan mengingat hal ini dan melakukan pengujian dalam interval waktu ini, melakukan pra-nol pada penghitung jam sebelum memulai.
Anda dapat melihat kode tes lengkapnya di sini . Semua tes dilakukan pada papan 32F769IDISCOVERY .
Memori VS. menulis kembali
Jadi mari kita mulai dengan beberapa tes yang sangat sederhana.
Kami hanya secara konsisten menulis ke memori.
dst = (uint8_t *) DATA_ADDR;
for (i = 0; i < ITERS * 8; i++) {
for (j = 0; j < DATA_LEN; j++) {
*dst = VALUE;
dst++;
}
dst -= DATA_LEN;
}
Kami juga menulis secara berurutan ke memori, tetapi tidak satu byte pada satu waktu, tetapi memperluas loop sedikit.
for (i = 0; i < ITERS * BLOCKS * 8; i++) {
for (j = 0; j < BLOCK_LEN; j++) {
*dst = VALUE;
*dst = VALUE;
*dst = VALUE;
*dst = VALUE;
dst++;
}
dst -= BLOCK_LEN;
}
Kami juga menulis secara berurutan ke memori, tetapi sekarang kami juga akan menambahkan bacaan.
for (i = 0; i < ITERS * BLOCKS * 8; i++) {
dst = (uint8_t *) DATA_ADDR;
for (j = 0; j < BLOCK_LEN; j++) {
val = VALUE;
*dst = val;
val = *dst;
dst++;
}
}
Jika Anda menjalankan ketiga tes ini, mereka akan memberikan hasil yang persis sama, apa pun mode yang Anda pilih:
mode: nc, iters=100, data len=65536, addr=0x60100000
Test1 (Sequential write):
0s 728ms
Test2 (Sequential write with 4 writes per one iteration):
7s 43ms
Test3 (Sequential read/write):
1s 216ms
Dan ini masuk akal, SDRAM tidak terlalu lambat, terutama jika Anda mempertimbangkan buffer internal FMC yang melaluinya terhubung. Meskipun demikian, saya mengharapkan sedikit variasi dalam angkanya, tetapi ternyata itu tidak ada dalam tes ini. Baiklah, mari kita pikirkan lebih jauh.
Mari kita coba "merusak" kehidupan SDRAM dengan mencampurkan baca dan tulis. Untuk melakukan ini, mari perluas loop dan tambahkan hal yang umum dalam praktiknya seperti penambahan elemen array:
for (i = 0; i < ITERS * BLOCKS; i++) {
for (j = 0; j < BLOCK_LEN; j++) {
// 16 lines
arr[i]++;
arr[i]++;
***
arr[i]++;
}
}
Hasil:
: 4s 743ms
Write-back: : 4s 187ms
Sudah lebih baik - dengan cache ternyata menjadi setengah detik lebih cepat. Mari kita coba memperumit pengujian lebih lanjut - tambahkan akses dengan indeks "renggang". Misalnya, dengan satu indeks:
for (i = 0; i < ITERS * BLOCKS; i++) {
for (j = 0; j < BLOCK_LEN; j++) {
arr[i + 0 ]++;
***
arr[i + 3 ]++;
arr[i + 4 ]++;
arr[i + 100]++;
arr[i + 6 ]++;
arr[i + 7 ]++;
***
arr[i + 15]++;
}
}
Hasil:
: 11s 371ms
Write-back: : 4s 551ms
Sekarang perbedaan dengan cache menjadi lebih dari terlihat! Dan sebagai pelengkap, kami memperkenalkan indeks kedua seperti:
for (i = 0; i < ITERS * BLOCKS; i++) {
for (j = 0; j < BLOCK_LEN; j++) {
arr[i + 0 ]++;
***
arr[i + 4 ]++;
arr[i + 100]++;
arr[i + 6 ]++;
***
arr[i + 9 ]++;
arr[i + 200]++;
arr[i + 11]++;
arr[i + 12]++;
***
arr[i + 15]++;
}
}
Hasil:
: 12s 62ms
Write-back: : 4s 551ms
Kami melihat bagaimana waktu untuk memori non-cache telah bertambah hampir satu detik, sedangkan untuk cache tetap sama.
Menulis alokasikan VS. tidak ada alokasi tulis
Sekarang mari kita berurusan dengan mode "alokasi tulis". Bahkan lebih sulit untuk melihat perbedaannya di sini, karena jika dalam situasi antara memori non-cache dan "tulis kembali" mereka menjadi terlihat jelas mulai dari pengujian ke-4, perbedaan antara "alokasi tulis" dan "tidak ada alokasi tulis" belum terungkap oleh pengujian. Mari kita pikirkan - kapan "alokasi tulis" akan lebih cepat? Misalnya, saat Anda memiliki banyak operasi tulis ke lokasi memori berurutan, dan ada sedikit pembacaan dari lokasi memori tersebut. Dalam kasus ini, dalam mode "tidak ada alokasi tulis", kami akan menerima kesalahan konstan, dan elemen yang salah akan dimuat ke cache dengan membaca. Mari kita simulasikan situasi ini:
for (i = 0; i < ITERS * BLOCKS; i++) {
for (j = 0; j < BLOCK_LEN; j++) {
arr[j + 0 ] = VALUE;
***
arr[j + 7 ] = VALUE;
arr[j + 8 ] = arr[i % 1024 + (j % 256) * 128];
arr[j + 9 ] = VALUE;
***
arr[j + 15 ] = VALUE;
}
}
Di sini, 15 dari 16 record disetel ke konstanta VALUE, sementara pembacaan dilakukan dari elemen yang berbeda (dan tidak terkait dengan penulisan) arr [i% 1024 + (j% 256) * 128]. Ternyata dengan strategi alokasi tanpa tulis, hanya elemen ini yang akan dimuat ke cache. Alasan mengapa pengindeksan seperti itu digunakan (i% 1024 + (j% 256) * 128) adalah "penurunan kecepatan" FMC / SDRAM. Karena akses memori pada alamat yang sangat berbeda (tidak berurutan) dapat mempengaruhi kecepatan kerja secara signifikan.
Hasil:
Write-back : 4s 720ms
Write-back no write allocate: : 4s 888ms
Akhirnya, kami mendapat perbedaan, meskipun tidak begitu terlihat, tetapi sudah terlihat. Artinya, hipotesis kami telah dikonfirmasi.
Dan akhirnya, kasus yang paling sulit, menurut pendapat saya. Kami ingin memahami kapan "tidak ada alokasi tulis" lebih baik daripada "alokasi tulis". Yang pertama lebih baik jika kita "sering" merujuk ke alamat yang tidak akan kita gunakan dalam waktu dekat. Data seperti itu tidak perlu di-cache.
Pada pengujian berikutnya, dalam kasus "alokasi tulis", data akan diisi pada baca dan tulis. Saya membuat array 64 KB "arr2", jadi cache akan di-flush untuk menukar data baru. Dalam kasus "tidak ada alokasi tulis", saya membuat array "arr" dari 4096 byte, dan hanya itu yang akan masuk ke cache, yang berarti bahwa data cache tidak akan dibuang ke memori. Karena ini, kami akan mencoba untuk mendapatkan setidaknya kemenangan kecil.
arr = (uint8_t *) DATA_ADDR;
arr2 = arr;
for (i = 0; i < ITERS * BLOCKS; i++) {
for (j = 0; j < BLOCK_LEN; j++) {
arr2[i * BLOCK_LEN ] = arr[j + 0 ];
arr2[i * BLOCK_LEN + j*32 + 1 ] = arr[j + 1 ];
arr2[i * BLOCK_LEN + j*64 + 2 ] = arr[j + 2 ];
arr2[i * BLOCK_LEN + j*128 + 3] = arr[j + 3 ];
arr2[i * BLOCK_LEN + j*32 + 4 ] = arr[j + 4 ];
***
arr2[i * BLOCK_LEN + j*32 + 15] = arr[j + 15 ];
}
}
Hasil:
Write-back : 7s 601ms
Write-back no write allocate: : 7s 599ms
Dapat dilihat bahwa mode "tulis kembali" "alokasi tulis" sedikit lebih cepat. Tetapi yang utama adalah itu lebih cepat.
Saya tidak mendapatkan peragaan yang lebih baik, tetapi saya yakin ada situasi praktis di mana perbedaannya lebih nyata. Pembaca dapat menyarankan opsi mereka sendiri!
Contoh praktis
Mari beralih dari contoh sintetik ke nyata.
ping
Salah satu cara yang paling sederhana adalah ping. Mudah untuk memulai, dan waktu dapat dilihat langsung di tuan rumah. Embox dibangun dengan pengoptimalan -O2. Izinkan saya memberi Anda hasilnya segera:
: ~0.246 c
Write-back : ~0.140 c
Opencv
Contoh lain dari masalah nyata yang ingin kami coba dengan subsistem cache adalah OpenCV di STM32F7 . Dalam artikel itu, ditunjukkan bahwa sangat mungkin untuk diluncurkan, tetapi kinerjanya cukup rendah. Untuk demonstrasi, kami akan menggunakan contoh standar yang mengekstrak batas berdasarkan filter Canny. Mari kita ukur waktu berjalan dengan dan tanpa cache (baik D-cache dan I-cache).
gettimeofday(&tv_start, NULL);
cedge.create(image.size(), image.type());
cvtColor(image, gray, COLOR_BGR2GRAY);
blur(gray, edge, Size(3,3));
Canny(edge, edge, edgeThresh, edgeThresh*3, 3);
cedge = Scalar::all(0);
image.copyTo(cedge, edge);
gettimeofday(&tv_cur, NULL);
timersub(&tv_cur, &tv_start, &tv_cur);
Tanpa cache:
> edges fruits.png 20
Processing time 0s 926ms
Framebuffer: 800x480 32bpp
Image: 512x269; Threshold=20
Dengan cache:
> edges fruits.png 20
Processing time 0s 134ms
Framebuffer: 800x480 32bpp
Image: 512x269; Threshold=20
Artinya, akselerasi 926ms dan 134ms hampir 7 kali lipat.
Bahkan kita sering ditanya tentang OpenCV di STM32, khususnya apa performanya. Ternyata FPS memang tidak tinggi, tapi 5 frame per detik cukup realistis untuk didapatkan.
Bukan memori yang dapat di-cache atau di-cache, tetapi dengan cache tidak valid?
DMA digunakan secara luas di perangkat nyata, tentu saja, kesulitan terkait dengannya, karena Anda perlu menyinkronkan memori bahkan untuk mode "tulis". Ada keinginan alami untuk mengalokasikan sepotong memori yang tidak akan di-cache dan menggunakannya saat bekerja dengan DMA. Sedikit teralihkan. Di Linux, ini dilakukan oleh fungsi melalui dma_coherent_alloc () . Dan ya, ini adalah metode yang sangat efektif, misalnya, saat bekerja dengan paket jaringan di OS, data pengguna melewati tahap pemrosesan yang besar sebelum mencapai driver, dan di driver, data yang disiapkan dengan semua header disalin ke buffer yang menggunakan memori non-cache.
Adakah kasus ketika clean / invalidate lebih disukai pada driver dengan DMA? Ya ada. Misalnya, memori video, yang mendorong kitaperhatikan lebih dekat cara kerja cache (). Dalam mode buffering ganda, sistem memiliki dua buffer, di mana ia menarik secara bergantian, dan kemudian memberikannya ke pengontrol video. Jika Anda membuat memori semacam itu tidak dapat disimpan dalam cache, maka akan ada penurunan kinerja. Oleh karena itu, lebih baik melakukan pembersihan sebelum mengirimkan buffer ke pengontrol video.
Kesimpulan
Kami menemukan sedikit tentang berbagai jenis cache di ARMv7m: tulis-balik, tulis-tayang, serta setelan "alokasi tulis" dan "tanpa alokasi tulis". Kami membuat pengujian sintetis di mana kami mencoba untuk mengetahui kapan satu mode lebih baik dari yang lain, dan juga mempertimbangkan contoh praktis dengan ping dan OpenCV. Di Embox, kami baru saja mengerjakan topik ini, jadi subsistem terkait masih dikerjakan. Keuntungan menggunakan cache pasti terlihat.
Semua contoh dapat dilihat dan direproduksi dengan membangun Embox dari repositori terbuka.
PS
Jika Anda tertarik dengan topik pemrograman sistem dan OSDev, maka konferensi OS Day akan diadakan besok ! Tahun ini online, jadi jangan lewatkan mereka yang menginginkannya! Embox akan tampil besok pukul 12.00