Özel Veri Yapıları

Özel veri türleri tanımlama kabiliyeti, programcıların kodlarını okunabilirliği ve sürdürülebilirliği artıracak şekilde yapılandırmasına olanak tanıdığından, modern bir programlama dilinin ayırt edici özelliklerinden biridir. MPI standardı programlama dillerinin hem bu özelliğini desteklemek hem de işlemciler arasında transfer edilen mesaj sayısını en aza indirebilmek için özel veri yapılarını desteklemektedir.

Önceklikle MPI’ın hangi basit data yapılarını desteklediğini görelim:

Desteklenen Data Yapıları

C programlama dilinde veri tipleri, standart tarafından tanımlanmış ve derleyici tarafından icra edilen, primitif yapılardır. MPI data yapıları ise derleyici tarafından farklı veri yapıları olarak gözükmez, özel veri yapılarının hepsi derleyici tarafından MPI_Datatype‘ın türleri olarak algılanmaktadır. Bu yüzden özel veri yapılarını kullanırken dikkat edilmesi gereken unsurlar bulunmaktadır.

C programa dilinde özel bir veri yapısı aşağıdaki gibi tanımlanabilir:

struct pair{
    int first,
    char second
}

Bu şekilde tanımlanan pair veri yapısı yeni bir veri yapısıdır. C standardı bu yeni data yapısının sorunsuz bir şekilde kullanılması için gereklilikleri belirler ve derleyici de gereken makine kodunu oluşturur. Böylece kullanıncı için yeni oluşturulan bu veri yapısının temel veri yapılarından bir farkı yoktur, fakat bunların hepsi derleme zamanında gerçekleşir.

MPI’da özle veri yapılarını kullanmak için kullanıcının heterojen mimarilerde sorunsuz bir şekilde mesaj yollayıp alabilmesi için alt düzey bilgiler vermesi gerekmektedir.

Özel Veri Yapılarının MPI’da Temsil Edilişi

Veri türü imzası yeni oluşturulan ver türündeki basit verilerin türlerini depolar.

$Type signature[𝚃]=[𝙳𝚊𝚝𝚊𝚝𝚢𝚙𝚎0,…,𝙳𝚊𝚝𝚊𝚝𝚢𝚙𝚎𝑛−1]$

Tip haritası, MPI tarafından algılan data türlerini anahtar ve bunların bayt cinsinden büyülüklerini değer olarak tutar.

$Typemap[𝚃]={𝙳𝚊𝚝𝚊𝚝𝚢𝚙𝚎0:Displacement0,…,𝙳𝚊𝚝𝚊𝚝𝚢𝚙𝚎𝑛−1:Displacement𝑛−1}$

Yer displacements, veri tipinin tanımladığı arabelleğe görecedir.

Bir int‘nin 4 bayt bellek aldığını varsayarsak, pair veri türünün tip haritası şöyle olur:

$Typemap[𝙿𝚊𝚒𝚛]={𝚒𝚗𝚝:0,𝚌𝚑𝚊𝚛:4}$

Tür haritası ve imzası bilgisi, türün MPI’da kullanılabilmesi için yeterli değildir. Temel alınan programlama dili, temel veri türlerinin mimariye özgü hizalanmasını zorunlu kılabilir. Türü MPI’a kaydedebilmek için birkaç konsepte daha ihtiyacımız var. Bir tip haritası, 𝑚, verildiğinde aşağıdakileri tanımlayabiliriz:

Alt Sınır:

Veri türünün kapsadığı ilk baytı temsil eder.

$LB[𝑚]=min_𝑗[Displacement_𝑗]$

Üst Sınır:

$UB[𝑚]=max_𝑗[Displacement_𝑗+𝚜𝚒𝚣𝚎𝚘𝚏(Datatype_𝑗)]+Padding$

Boyut:

$Extent[𝑚]=UB[𝑚]−LB[𝑚]$

C programlama dilinde verilerin bellekte düzgün tanımlanmış adreslerde olması gerekir, başka bir deyişle verilerin hizalanması gerekir. Herhangi bir öğenin bayt cinsinden adresi, o öğenin bayt cinsinden boyutunun katı olmalıdır. Buna doğal hizalama denir. pair veri yapımız için ilk öğe bir int‘dir ve 4 baytlık yer kaplar. Bir int, 4 bayt sınırlarına hizalanır: bellekte yeni bir int tahsis ederken, derleyici hizalama sınırına ulaşmak için dolgu ekler. second bir karakterdir ve sadece 1 bayt gerektirir, bu yüzden de her adrese tanımlanabilir.

$p𝚊𝚒𝚛.𝚏𝚒𝚛𝚜𝚝→Displacement0=0,𝚜𝚒𝚣𝚎𝚘𝚏(𝚒𝚗𝚝)=4$

$p𝚊𝚒𝚛.𝚜𝚎𝚌𝚘𝚗𝚍→Displacement1=4,𝚜𝚒𝚣𝚎𝚘𝚏(𝚌𝚑𝚊𝚛)=1$

Başka bir pair öğesi eklerken, bir sonraki int baytının uygun bir adresten başlayabilmesi için, 3 baytlık bir dolgu ile hizalama sınırına ulaşmamız gerekir. Böylece:

$LB[𝙿𝚊𝚒𝚛]=min[0,4]=0$

$UB[𝙿𝚊𝚒𝚛]=max[0+4,4+1]+3=8$

$Extent[𝙿𝚊𝚒𝚛]=UB[𝙿𝚊𝚒𝚛]−LB[𝙿𝚊𝚒𝚛]=8$

Bir sonraki bölümde yukarıda anlatılan detayları göz önünde bulundurarak bir MPI veri türü tanımlaycağız.

MPI ile özel veri yapısı yaratma

Yukarıda C kodunu gösterdiğimiz özel veri türü, pair, MPI’da tanımlamak için öncelikle verinin imza tipini belirtiyoruz.

MPI_Datatype typesig[2] = {MPI_INT, MPI_CHAR};

pair‘in içerdiği verilerin sayısını belirtiyoruz.

int block_lengths[2] = {1, 1};

pair veri türünün içinde barındırdığı verilerin başlangıç adreslerini displacements‘ta depoluyoruz. Bu yukarda bahsettiğimiz sebeplerden dolayı gerçekleştirmemiz gereken bir adım.

MPI_Aint displacements[2];
MPI_Get_address(&my_pair.first, &displacements[0]);
MPI_Get_address(&my_pair.second, &displacements[1]);

Yukarda örneğini verdiğimiz pair veri türünün iki alanı var dolayısıyla MPI_Type_create_struct çağrısında count = 2‘dir.

MPI_Datatype mpi_pair;
MPI_Type_create_struct(2, block_lengths, displacements, typesig, &mpi_pair);
MPI_Type_commit(&mpi_pair);

Veri yapısının kullandıktan sonra serbest bırakıyoruz.

MPI_Type_free(&mpi_pair);

Paketleme ve Çözme

MPI yapıları aynı olmayan verileri birlikte yollayabilmek için paketleme ve çözme alt yapısı sağlamaktadır. Böylece birlikte yollamak istediğimiz farklı veri yapılarını her zaman yeni bir veri yapısı tanımlayarak yollamak zorunda kalmayız. Paketleme sonucu ortaya çıkan paketlenmiş arabellek MPI_PACKED türündedir ve MPI tarafından tanınan herhangi bir tür heterojen temel veri türü koleksiyonunu içerebilir.

/assets/openmpi-education/images/pack-unpack.png

Yukarıdaki figürden de gösterildiği gibi farklı veri yapılarına ait olan değerler tek bir mesaja bitişik bir şekilde paketlenir ve alıcı da aynı şekilde çözülür.

MPI_Pack

int MPI_Pack(const void *inbuf,
             int incount,
             MPI_Datatype datatype,
             void *outbuf,
             int outsize,
             int *position,
             MPI_Comm comm)

inbuf: yollayacağımız verinin işaretçisi

incount: paketleyeceğimiz veri miktarı

datatype: paketleyeceğimiz verinin türü

outbuf: yollayacağımız mesajı temsil eden arabelleğinin işaretçisi

outsize: yollayacağımız mesajın büyüklüğü

position: outbuf içindeki konumları tanımlayan bir giriş/çıkış parametresidir. inbuf‘taki veriler outbuf + *position‘a kopyalanacaktır. Fonksiyon geri döndükten sonra, *position değeri, çıkış verisindeki yeni kopyalanan verileri izleyen ilk konumu gösterir. Bu, MPI_Pack‘e bir sonraki çağrıya konum olarak geçmek için kullanışlıdır.

comm: programlar arası iletişimi sağlayan obje

MPI_Unpack

int MPI_Unpack(const void *inbuf,
               int insize,
               int *position,
               void *outbuf,
               int outcount,
               MPI_Datatype datatype,
               MPI_Comm comm)

inbuf: aldığımız mesajı temsil eden arabelleğin işaretçisi

insize: aldığımız mesajın büyüklüğü

position: outbuf içindeki konumları tanımlayan bir giriş/çıkış parametresidir. inbuf veriler outbuf + *position kopyalanacaktır. Fonksiyon geri döndükten sonra, *position değeri, çıkış verisindeki yeni kopyalanan verileri izleyen ilk konumu gösterir. Bu, MPI_Pack‘e bir sonraki çağrıya konum olarak geçmek için kullanışlıdır.

outbuf: çıkardığımız veriyi temsil eden arabelleğin işaretçisi

outcount: çıkardığımız verideki eleman miktarı

datatype: çıkardığımız verinin türü

comm: programlar arası iletişimi sağlayan obje

Pokemonlar ile Paketleme/Çıkarma Örneği

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <mpi.h>

#define STRLEN 25

/*
  Bu örnekte sanal bir pokemonun hareketlerini birer mesaj olarak göndereceğiz.
  Bu niteliklerin her birisini paketleyerek yollayıp, aldığımızda çıkartacağız.
*/

int main(int argc, char *argv[]) {
  int rank;
  int size;
  // marker used by MPI_Pack and MPI_Unpack
  int position;

  // pokemonun ismi
  char name[STRLEN];
  // pokemonun canı
  double life_points;
  // pokemunun gerçekleştirdiği zarar
  int damage;
  // güç katsayısı
  double multiplier;

  // stdio.h deki BUFSIZ'ı yeterli büyüklükte olduğunu düşünerek kullanıyoruz
  char message[BUFSIZ];

  MPI_Init(&argc, &argv);

  MPI_Comm comm = MPI_COMM_WORLD;

  MPI_Comm_size(comm, &size);
  MPI_Comm_rank(comm, &rank);

  // 0'ıncı sıraya sahip olan işlem diğer bütün işlemlere pokemunun hareketini temsil eden bir mesaj yollayacak
  if (rank == 0) {
    sprintf(name, "Blastoise");
    life_points = 150.0;
    damage = 40;
    multiplier = 1.32;

    position = 0;
    // paketleme işlemini gerçekleştiriyoruz
    MPI_Pack(&name, STRLEN, MPI_CHAR, message, BUFSIZ, &position, comm);

    printf("packed name, position = %d\n", position);

    MPI_Pack(&life_points, 1, MPI_DOUBLE, message, BUFSIZ, &position, comm);
    printf("packed life_points, position = %d\n", position);

    MPI_Pack(&damage, 1, MPI_INT, message, BUFSIZ, &position, comm);
    printf("packed damage, position = %d\n", position);

    MPI_Pack(&multiplier, 1, MPI_DOUBLE, message, BUFSIZ, &position, comm);
    printf("packed multiplier, position = %d\n", position);

    // mesajı diğer bütün işlemlere yolluyoruz
    MPI_Bcast(message, BUFSIZ, MPI_PACKED, 0, comm);
  } else {
    // diğer bütün işlemlerde mesajı alıyoruz
    MPI_Bcast(message, BUFSIZ, MPI_PACKED, 0, comm);

    position = 0;
    // veri çıkarımına başlıyoruz
    // buarada yolladığımız mesajın uzunluğunu bilmeliyiz
    // bilmediğimiz durumlarda bu bilgiyi mesajın sonuna veya başına gömebiliriz
    MPI_Unpack(message, BUFSIZ, &position, &name, STRLEN, MPI_CHAR, comm);
    printf("unpacked name, position = %d\n", position);

    MPI_Unpack(message, BUFSIZ, &position, &life_points, 1, MPI_DOUBLE, comm);
    printf("unpacked life_points, position = %d\n", position);

    MPI_Unpack(message, BUFSIZ, &position, &damage, 1, MPI_INT, comm);
    printf("unpacked damage, position = %d\n", position);

    MPI_Unpack(message, BUFSIZ, &position, &multiplier, 1, MPI_DOUBLE, comm);
    printf("unpacked multiplier, position = %d\n", position);

    printf("rank %d:\n", rank);
    printf("  name = %s\n", name);
    printf("  life_points = %2.2f\n", life_points);
    printf("  damage = %d\n", damage);
    printf("  multiplier = %2.2f\n", multiplier);
  }

  MPI_Finalize();

  return EXIT_SUCCESS;
}