Array melintasi batas bahasa

Kekontiguan, bentuk eksplisit, dan urutan penyimpanan kolom utama saat array melintasi batas bahasa.

Daftar isi

Ketika sebuah array diteruskan dari Fortran ke C, yang sampai di pihak C hanya alamat elemen pertamanya, sedangkan bentuk dan urutan interpretasinya tidak dibawa sama sekali. Kesepakatan tentang tata letak memori oleh karena itu menjadi tanggung jawab penulis kode. Topik ini membahas perbedaan urutan kolom utama dan baris utama, bukti tata letak tersebut melalui selisih alamat elemen, serta aturan kekontiguan yang membuat sebuah argumen array aman dilintaskan. Dua program disertakan dan dijalankan, dan keduanya memperlihatkan perilaku yang tidak dilaporkan kompiler sebagai error.

Kolom utama versus baris utama#

Fortran menyimpan elemen array berurutan menurut kolom (column-major), sehingga a(2,1) terletak tepat setelah a(1,1) di memori, sedangkan C menyimpannya menurut baris (row-major). Array dua dimensi yang sama dengan demikian terbaca sebagai deretan angka yang berbeda oleh kedua bahasa. Cara yang aman adalah memperlakukan array Fortran sebagai deret satu dimensi di pihak C, lalu menghitung indeksnya sesuai urutan kolom utama.

Program berikut meneruskan matriks 2 x 3 yang sama ke dua prosedur C, yang masing-masing menafsirkan deret memorinya sebagai baris utama dan kolom utama.

fortran array.f90
program array_c
   use, intrinsic :: iso_c_binding
   implicit none
   interface
      subroutine cetak_baris(v, baris, kolom) bind(c, name="cetak_baris")
         import :: c_int, c_double
         real(c_double), intent(in) :: v(*)
         integer(c_int), value :: baris, kolom
      end subroutine cetak_baris
      subroutine cetak_kolom(v, baris, kolom) bind(c, name="cetak_kolom")
         import :: c_int, c_double
         real(c_double), intent(in) :: v(*)
         integer(c_int), value :: baris, kolom
      end subroutine cetak_kolom
   end interface
   real(c_double) :: nilai(2,3)
   nilai = reshape([1.0_c_double, 4.0_c_double, 2.0_c_double, 5.0_c_double, &
        & 3.0_c_double, 6.0_c_double], [2,3])
   print *, "tampilan Fortran:"
   print '(3F7.1)', nilai(1,:), nilai(2,:)
   flush(6)
   print *, "interpretasi baris utama di C:"
   flush(6)
   call cetak_baris(nilai, 2_c_int, 3_c_int)
   print *, "interpretasi kolom utama di C:"
   flush(6)
   call cetak_kolom(nilai, 2_c_int, 3_c_int)
end program array_c
c cetak.c
// tidak-dijalankan
#include <stdio.h>

void cetak_baris(const double *v, int baris, int kolom) {
    for (int i = 0; i < baris; i++) {
        for (int j = 0; j < kolom; j++) {
            printf("%6.1f", v[i * kolom + j]);
        }
        printf("\n");
    }
    fflush(stdout);
}

void cetak_kolom(const double *v, int baris, int kolom) {
    for (int i = 0; i < baris; i++) {
        for (int j = 0; j < kolom; j++) {
            printf("%6.1f", v[j * baris + i]);
        }
        printf("\n");
    }
    fflush(stdout);
}
bash
# kompilasi berkas C menjadi berkas objek
gcc -c cetak.c -o cetak.o
# sambungkan program Fortran dengan berkas objek C
gfortran array.f90 cetak.o -o app
# jalankan program hasil kompilasi
./app
text Keluaran
 tampilan Fortran:
    1.0    2.0    3.0
    4.0    5.0    6.0
 interpretasi baris utama di C:
   1.0   4.0   2.0
   5.0   3.0   6.0
 interpretasi kolom utama di C:
   1.0   2.0   3.0
   4.0   5.0   6.0

Deret memori yang diterima C adalah 1, 4, 2, 5, 3, 6, yaitu kolom pertama 1 dan 4, kolom kedua 2 dan 5, lalu kolom ketiga 3 dan 6. Prosedur cetak_baris menafsirkan deret itu sebagai baris utama sehingga menghasilkan baris yang tercampur, sedangkan cetak_kolom menghitung indeksnya dan mengembalikan matriks aslinya. Untuk elemen pada baris i dan kolom j yang dinomori mulai dari nol, kedua rumus indeks itu dinyatakan sebagai berikut.

baris utama: i×kolom+jkolom utama: j×baris+i \text{baris utama: } i \times \text{kolom} + j \qquad \text{kolom utama: } j \times \text{baris} + i

Rumus pertama dipakai cetak_baris, sedangkan rumus kedua dipakai cetak_kolom melalui ungkapan v[j * baris + i]. Alternatif lainnya adalah menyalin matriks dengan intrinsik transpose sebelum diteruskan, sehingga pihak C dapat memakai rumus baris utamanya, dengan biaya satu salinan memori pada setiap pemanggilan. Pernyataan flush(6) dan fflush(stdout) pada kedua berkas mengosongkan penyangga keluaran Fortran dan C yang terpisah, sehingga urutan keluaran sesuai dengan urutan eksekusi program.

Kekontiguan dan alamat elemen#

Argumen array yang interoperabel wajib menempati memori yang berurutan, dan bentuk yang menjaminnya adalah bentuk eksplisit seperti v(3) maupun bentuk diasumsikan seperti v(*) pada contoh di atas. Bagian array seperti a(:,2) tidak memiliki jaminan kekontiguan, sehingga tidak diteruskan langsung, tetapi disalin dahulu ke array kontigu. Standar Fortran 2008 menambahkan atribut contiguous untuk menegaskan janji kekontiguan pada argumen berbentuk asumsi, dan kompiler memanfaatkannya untuk menyalin atau mengoptimalkan akses. Tanpa janji tersebut, penerusan bagian array tetap sah di dalam Fortran, tetapi identitas memorinya tidak terjaga, dan itulah yang membuatnya tidak dapat dilintaskan ke C.

Aturan yang sama membuat elemen tunggal boleh diambil alamatnya, sedangkan bagian array tidak dapat diwakili satu alamat tunggal yang bermakna. Program berikut mengambil alamat dua elemen yang berdekatan secara diagonal dan menghitung selisihnya dalam byte.

fortran alamat-elemen.f90
program alamat_elemen
   use, intrinsic :: iso_c_binding
   implicit none
   real(c_double), target :: a(2,3)
   type(c_ptr) :: p12, p21
   integer(c_intptr_t) :: alamat12, alamat21
   a = reshape([1.0_c_double, 4.0_c_double, 2.0_c_double, 5.0_c_double, &
        & 3.0_c_double, 6.0_c_double], [2,3])
   p12 = c_loc(a(1,2))
   p21 = c_loc(a(2,1))
   alamat12 = transfer(p12, alamat12)
   alamat21 = transfer(p21, alamat21)
   print *, "selisih byte a(1,2) - a(2,1):", alamat12 - alamat21
end program alamat_elemen
bash
# kompilasi program pemeriksa alamat
gfortran alamat-elemen.f90 -o alamat-elemen
# jalankan program hasil kompilasi
./alamat-elemen
text Keluaran
 selisih byte a(1,2) - a(2,1):                    8

Elemen a(2,1) menempati posisi kedua pada memori dan a(1,2) menempati posisi ketiga, sehingga selisihnya tepat 8 byte, yaitu ukuran satu double. Angka tersebut membuktikan bahwa lintasan memori bergerak menurun di dalam kolom dahulu sebelum pindah kolom, dan bukan melintasi baris sebagaimana tata letak C. Ukuran 8 byte itu berlaku karena real(c_double) menempati 64 bit, dan selisih yang sama dihasilkan oleh setiap pasangan elemen berurutan pada kolom yang sama. Alamat dihitung dengan c_loc atas elemen scalar yang arraynya beratribut target, lalu transfer mengonversi c_ptr menjadi integer(c_intptr_t) agar selisihnya dapat dihitung.

Kesalahan umum#

  • Rumus indeks yang mengikuti bahasa yang salah. Formula v[i * kolom + j] hanya benar untuk data baris utama, sedangkan array Fortran memerlukan v[j * baris + i]. Kedua rumus diuji pada program di atas, dan keluarannya memperlihatkan bedanya secara nyata.
  • Meneruskan bagian array yang tidak kontigu. Bagian seperti a(:,2) disalin kompiler ke tempat sementara, sehingga alamat yang diterima C tidak merujuk pada array aslinya dan perubahan dari C hilang. Array disalin dahulu ke array kontigu atau diteruskan per kolom sebagai array satu dimensi.
  • Mengambil alamat bagian array dengan c_loc. Argumen c_loc wajib scalar beratribut target, sehingga pola yang benar adalah c_loc(a(1,2)) untuk satu elemen, sebagaimana dipakai pada program di atas.
  • Asal indeks yang berbeda. C menomori elemen mulai dari nol, sedangkan Fortran mulai dari satu, sehingga rumus indeks pada pihak C bekerja atas nomor baris dan kolom yang telah dikurangi satu apabila koordinat Fortran diteruskan apa adanya. Program di atas terhindar dari kekeliruan ini karena kedua prosedur C langsung menerima jumlah baris dan jumlah kolom, bukan indeks elemen tertentu.

Ringkasan#

  • Fortran menyimpan array menurut kolom dan C menurut baris, sehingga array yang sama terbaca sebagai deret yang berbeda bila urutannya tidak disepakati.
  • Argumen array yang interoperabel wajib kontigu dengan bentuk eksplisit atau diasumsikan, dan selisih alamat elemen sebesar satu ukuran tipe membuktikan arah kolom utama.
  • Keluaran campuran Fortran dan C memerlukan flush dan fflush karena kedua bahasa memiliki penyangga keluaran yang terpisah.

Referensi#