Cilk

Cilk
Paradygmat imperatyw ( proceduralny ), ustrukturyzowany , równoległy
Zaprojektowany przez Laboratorium Informatyki MIT
Deweloper Intel
Po raz pierwszy pojawiły się 1994
Dyscyplina pisania statyczny , słaby , oczywisty
Strona internetowa cilk .mit .edu
Dialekty
Cilk++, Cilk Plus, OpenCilk
Pod wpływem
C
Pod wpływem
OpenMP 3.0, Rayon ( biblioteka Rust )
OpenCilk
Zaprojektowany przez MIT
Deweloper MIT
Po raz pierwszy pojawiły się 2020
Wersja stabilna
2.0.1 / 3 września 2022 r . ; 6 miesięcy temu ( 03.09.2022 )
system operacyjny Uniksopodobny , macOS
Licencja MIT
Strona internetowa www.opencilk.org _ _
Cilk Plus
Zaprojektowany przez Intel
Deweloper Intel
Po raz pierwszy pojawiły się 2010
Wersja stabilna
1.2 / 9 września 2013 ; 9 lat temu ( 09.09.2013 )
Rozszerzenia nazw plików (Tak samo jak C lub C++)
Strona internetowa http://cilkplus.org/

Cilk , Cilk++ , Cilk Plus i OpenCilk to języki programowania ogólnego przeznaczenia przeznaczone do wielowątkowych obliczeń równoległych . Oparte są na C i C++ , które rozszerzają o konstrukcje wyrażające pętle równoległe oraz idiom fork-join .

Pierwotnie opracowany w latach 90. w Massachusetts Institute of Technology (MIT) w grupie Charlesa E. Leisersona , Cilk został później skomercjalizowany jako Cilk ++ przez firmę spinoff, Cilk Arts. Firma ta została następnie przejęta przez firmę Intel , która zwiększyła kompatybilność z istniejącym kodem C i C++, nazywając wynik Cilk Plus. Po tym, jak Intel przestał wspierać Cilk Plus w 2017 roku, MIT ponownie rozwija Cilk w formie OpenCilk.

Historia

MIT Cilk

Język programowania Cilk wyrósł z trzech oddzielnych projektów w MIT Laboratory for Computer Science:

  • Teoretyczne prace nad szeregowaniem aplikacji wielowątkowych.
  • StarTech - program do gry w szachy równoległe zbudowany do działania na maszynie łączącej Thinking Machines Corporation model CM-5.
  • PCM/Threaded-C – oparty na C pakiet do planowania wątków w stylu przekazywania kontynuacji w CM-5

W kwietniu 1994 roku te trzy projekty zostały połączone i ochrzczone „Cilk”. Nazwa Cilk nie jest akronimem, ale aluzją do „ładnych nici” ( silk ) i języka programowania C. Kompilator Cilk-1 został wydany we wrześniu 1994 roku.

Oryginalny język Cilk był oparty na ANSI C , z dodatkiem specyficznych dla Cilk słów kluczowych w celu sygnalizacji równoległości. Kiedy słowa kluczowe Cilk są usuwane z kodu źródłowego Cilk, rezultatem zawsze powinien być prawidłowy program C, zwany serial elision (lub C elision ) pełnego programu Cilk, z taką samą semantyką jak program Cilk działający na pojedynczym procesorze. Pomimo kilku podobieństw, [ które? ] Cilk nie jest bezpośrednio powiązany z Concurrent C firmy AT&T Bell Labs.

Cilk został zaimplementowany jako tłumacz do C, ukierunkowany na GNU C Compiler (GCC). Ostatnia wersja, Cilk 5.4.6, jest dostępna w MIT Computer Science and Artificial Intelligence Laboratory (CSAIL), ale nie jest już obsługiwana.

Wizytówką możliwości Cilk był program do gry w szachy równoległe Cilkchess, który zdobył kilka nagród w szachach komputerowych w latach 90., w tym Otwarte Holenderskie Mistrzostwa w Szachach Komputerowych w 1996 roku.

Cilk Arts i Cilk++

przed ok. 2006 rynek Cilk był ograniczony do komputerów o wysokiej wydajności. Pojawienie się procesorów wielordzeniowych w głównym nurcie komputerów oznaczało, że każdego roku wysyłano setki milionów nowych komputerów równoległych. Cilk Arts powstało, aby wykorzystać tę szansę: w 2006 roku Leiserson uruchomił Cilk Arts, aby stworzyć i wprowadzić na rynek nowoczesną wersję Cilk, która obsługuje komercyjne potrzeby nadchodzącej generacji programistów. Firma zamknęła rundę finansowania przedsięwzięć typu A w październiku 2007 r., a jej produkt, Cilk++ 1.0, został wprowadzony na rynek w grudniu 2008 r.

Cilk++ różni się od Cilk na kilka sposobów: wsparcie dla C++, wsparcie dla pętli i hiperobiektów – nowa konstrukcja zaprojektowana do rozwiązywania problemów związanych z wyścigiem danych stworzonym przez równoległy dostęp do zmiennych globalnych. Cilk++ było oprogramowaniem zastrzeżonym . Podobnie jak jego poprzednik, został zaimplementowany jako kompilator Cilk-to-C++. Obsługuje Microsoft i GNU.

Intel Cilk Plus

31 lipca 2009 r. firma Cilk Arts ogłosiła na swojej stronie internetowej, że jej produkty i zespół inżynierów są teraz częścią firmy Intel Corp. Na początku 2010 r. witryna Cilk pod adresem www.cilk.com zaczęła przekierowywać do witryny firmy Intel , oryginalna witryna Cilk nie jest już rozpoznawana jako host). Firmy Intel i Cilk Arts zintegrowały i udoskonaliły tę technologię, co zaowocowało wydaniem we wrześniu 2010 r. Intel Cilk Plus . Cilk Plus przyjmuje uproszczenia zaproponowane przez Cilk Arts w Cilk++, aby wyeliminować potrzebę stosowania kilku oryginalnych słów kluczowych Cilk, jednocześnie dodając możliwość odradzania funkcji i radzenia sobie ze zmiennymi zaangażowanymi w operacje redukcji. Cilk Plus różni się od Cilk i Cilk++ dodaniem rozszerzeń tablic, włączeniem do komercyjnego kompilatora (firmy Intel) oraz kompatybilnością z istniejącymi debuggerami.

Cilk Plus został po raz pierwszy zaimplementowany w kompilatorze Intel C++ wraz z wydaniem kompilatora Intel w Intel Composer XE 2010. [ Potrzebne źródło ] Implementacja typu open source ( na licencji BSD ) została wniesiona przez firmę Intel do GNU Compiler Collection (GCC), która dostarczona obsługa Cilk Plus w wersji 4.9, z wyjątkiem słowa kluczowego _Cilk_for , które zostało dodane w GCC 5.0. W lutym 2013 roku Intel ogłosił widelec Clang z obsługą Cilk Plus. Intel Compiler, ale nie implementacje open source, jest wyposażony w wykrywacz wyścigów i analizator wydajności.

Intel później zaprzestał tego, zalecając swoim użytkownikom przejście zamiast tego na korzystanie z OpenMP lub własnej biblioteki TBB firmy Intel do ich potrzeb w zakresie programowania równoległego.

Różnice między wersjami

W oryginalnej implementacji MIT Cilk pierwszym słowem kluczowym Cilk jest w rzeczywistości cilk , które identyfikuje funkcję napisaną w Cilk. Ponieważ procedury Cilk mogą bezpośrednio wywoływać procedury C, ale procedury C nie mogą bezpośrednio wywoływać ani tworzyć procedur Cilk, to słowo kluczowe jest potrzebne do odróżnienia kodu Cilk od kodu C. Cilk Plus usuwa to ograniczenie, jak również cilk , więc funkcje C i C++ mogą wywoływać kod Cilk Plus i odwrotnie.

Wycofanie Cilk Plus

W maju 2017 roku wydano GCC 7.1 i oznaczono obsługę Cilk Plus jako przestarzałą. Sam Intel ogłosił we wrześniu 2017 r., Że wycofa Cilk Plus wraz z wydaniem w 2018 r. Intel Software Development Tools. W maju 2018 roku wydano GCC 8.1 z usuniętą obsługą Cilk Plus.

OpenCilk

Po tym, jak Intel wycofał obsługę Cilk Plus, MIT zajął się rozwojem Cilk w implementacji OpenCilk, koncentrując się na rozwidleniu LLVM / Clang, obecnie określanym jako „Tapir”. OpenCilk pozostaje w dużej mierze kompatybilny z Intel Cilk Plus. Jego pierwsza stabilna wersja została wydana w marcu 2021 roku.

Funkcje językowe

Zasada leżąca u podstaw projektowania języka Cilk polega na tym, że programista powinien być odpowiedzialny za ujawnienie równoległości, identyfikując elementy, które można bezpiecznie wykonać równolegle; należy wtedy pozostawić środowisku wykonawczemu, zwłaszcza programowi planującemu , podjęcie decyzji podczas wykonywania, jak faktycznie podzielić pracę między procesory. Ponieważ te obowiązki są rozdzielone, program Cilk może działać bez przepisywania na dowolnej liczbie procesorów, w tym na jednym.

Równoległość zadań: odradzanie i synchronizacja

Głównym dodatkiem Cilk do C są dwa słowa kluczowe, które razem umożliwiają pisanie programów równoległych zadań.

  • Słowo kluczowe spawn poprzedzające wywołanie funkcji ( spawn f(x) ) wskazuje, że wywołanie funkcji ( f(x) ) może bezpiecznie działać równolegle z następującymi po nim instrukcjami w funkcji wywołującej. Należy zauważyć, że program planujący nie jest zobowiązany do równoległego uruchamiania tej procedury; słowo kluczowe jedynie ostrzega program planujący, że może to zrobić.
  • Instrukcja sync wskazuje, że wykonanie bieżącej funkcji nie może być kontynuowane, dopóki wszystkie wywołania wcześniej wywołanej funkcji nie zostaną zakończone. Jest to przykład barierowej .

(W Cilk Plus słowa kluczowe są zapisywane jako _Cilk_spawn i _Cilk_sync lub cilk_spawn i cilk_sync , jeśli uwzględniono nagłówki Cilk Plus.)

Poniżej znajduje się rekurencyjna implementacja funkcji Fibonacciego w Cilk, z równoległymi wywołaniami rekurencyjnymi, która demonstruje słowa kluczowe spawn i sync . Oryginalny Cilk wymagał, aby każda funkcja wykorzystująca te funkcje była opatrzona słowem cilk , które zniknęło od Cilk Plus. (Kod programu Cilk nie jest numerowany; numery zostały dodane tylko po to, aby ułatwić śledzenie dyskusji.)

    
        
         
    
     
         



          
    
 cilk  int  fib  (  int  n  )  {  if  (  n  <  2  )  {  return  n  ;  }  else  {  int  x  ,  y  ;              x  =  spawn  fib  (  n  -  1  ); 
             y  =  spawn  fib  (  n  -  2  ); 
        synchronizacja  ; 
 zwróć  x  +  y  ;  }  } 

Gdyby ten kod został wykonany przez pojedynczy procesor w celu określenia wartości fib(2) , ten procesor utworzyłby ramkę dla fib(2) i wykonałby linie od 1 do 5. W linii 6 utworzyłby spacje w ramce do trzymaj wartości x i y . W linii 8 procesor musiałby zawiesić bieżącą ramkę, utworzyć nową ramkę w celu wykonania procedury fib(1) , wykonać kod tej ramki aż do osiągnięcia instrukcji return, a następnie wznowić ramkę fib(2) z wartość fib(1) umieszczona w zmiennej x fib(2) . W następnym wierszu musiałby się ponownie zawiesić, aby wykonać fib(0) i umieścić wynik w zmiennej y fib (2) .

gdy kod jest wykonywany na maszynie wieloprocesorowej , wykonanie przebiega inaczej. Jeden procesor rozpoczyna wykonywanie fib(2) ; kiedy jednak dociera do linii 8, słowo spawn modyfikujące wywołanie fib(n-1) mówi procesorowi, że może bezpiecznie przekazać zadanie drugiemu procesorowi: ten drugi procesor może utworzyć ramkę dla fib(1) , wykonać jego kod i zapisz jego wynik w ramce fib(2) po jego zakończeniu; pierwszy procesor kontynuuje wykonywanie kodu fib(2) w tym samym czasie. Procesor nie jest zobowiązany do przypisania zrodzionej procedury w innym miejscu; jeśli maszyna ma tylko dwa procesory, a drugi jest nadal zajęty fib(1) , kiedy procesor wykonujący fib(2) dochodzi do wywołania procedury, pierwszy procesor zawiesi fib(2) i sam wykona fib(0) , jak tak by było, gdyby był to jedyny procesor. Oczywiście, jeśli dostępny jest inny procesor, zostanie on uruchomiony, a wszystkie trzy procesory wykonają jednocześnie oddzielne ramki.

(Poprzedni opis nie jest całkowicie dokładny. Chociaż powszechna terminologia dotycząca Cilk odnosi się do procesorów podejmujących decyzję o przekazywaniu pracy innym procesorom, w rzeczywistości to program planujący przypisuje procesorom procedury do wykonania, używając polityki zwanej work- kradzież , opisana później).

Gdyby procesor wykonujący fib(2) wykonał linię 13, zanim oba pozostałe procesory zakończyły swoje ramki, wygenerowałby niepoprawny wynik lub błąd; fib(2) próbowałby dodać wartości przechowywane w x i y , ale brakowałoby jednej lub obu tych wartości. To jest cel słowa sync , które widzimy w wierszu 11: mówi ono procesorowi wykonującemu ramkę, że musi zawiesić swoje własne wykonywanie, dopóki nie powrócą wszystkie wywołane przez niego wywołania procedur. Kiedy fib(2) może przejść poza instrukcję sync w wierszu 11, może to być spowodowane tym, że fib(1) i fib(0) zakończyły i umieściły swoje wyniki w x i y , dzięki czemu można bezpiecznie wykonywać obliczenia na tych wyniki.

Powyższy przykład kodu wykorzystuje składnię Cilk-5. Oryginalny Cilk (Cilk-1) używał raczej innej składni, która wymagała programowania w jawnym stylu przekazywania kontynuacji , a przykłady Fibonacciego wyglądają następująco:

     

        
         
    
     
           
           
            
            
    


       

        
 fib  wątku  (  cont  int  k  ,  int  n  )  {  if  (  n  <  2  )  {  send_argument  (  k  ,  n  );  }  else  {  cd  int  x  ,  y  ;  spawn_następna  suma  (  k  ,  ?  x  ,  ?  y  );  spawn  fib  (  x  ,  n  -  1  );  spawn  fib  (  y  ,  n  -  2  );  }  }  suma  wątków  (  cont  int  k  ,  int  x  ,  int  y  )  {  send_argument  (  k  ,  x  +  y  );  } 

Wewnątrz przypadku rekurencyjnego fib słowo kluczowe spawn_next wskazuje na utworzenie wątku następcy (w przeciwieństwie do wątków potomnych tworzonych przez spawn ), który wykonuje podprogram sumy po odczekaniu na wypełnienie zmiennych kontynuacji x i y przez rekurencję połączenia. Przypadek podstawowy i sum używają operacji send_argument(k, n) , aby ustawić ich zmienną kontynuacji k na wartość n , skutecznie „zwracając” wartość do wątku następcy.

Wloty

Dwa pozostałe słowa kluczowe Cilk są nieco bardziej zaawansowane i dotyczą wykorzystania wlotów . Zwykle, gdy procedura Cilk jest wywoływana, może ona zwrócić swoje wyniki do procedury nadrzędnej tylko poprzez umieszczenie tych wyników w zmiennej w ramce rodzica, ponieważ w przykładzie przypisaliśmy wyniki wywołań naszej wywołanej procedury do x i y .

Alternatywą jest użycie wlotu. Wlot jest wewnętrzną funkcją procedury Cilk, która obsługuje wyniki zrodzonego wywołania procedury w miarę ich powrotu. Jednym z głównych powodów używania wlotów jest to, że wszystkie wloty procedury mają gwarancję, że będą działać atomowo względem siebie i procedury nadrzędnej, unikając w ten sposób błędów, które mogłyby wystąpić, gdyby wielokrotne procedury zwracające próbowały zaktualizować te same zmienne w ramkę nadrzędną w tym samym czasie.

  • Słowo kluczowe inlet identyfikuje funkcję zdefiniowaną w procedurze jako wlot.
  • Słowo kluczowe abort może być użyte tylko wewnątrz wlotu; informuje program planujący, że wszelkie inne procedury, które zostały uruchomione przez procedurę nadrzędną, można bezpiecznie przerwać.

Wloty zostały usunięte, gdy Cilk stał się Cilk++ i nie występują w Cilk Plus.

Pętle równoległe

Cilk++ dodał dodatkową konstrukcję, pętlę równoległą, oznaczoną cilk_for w Cilk Plus. Te pętle wyglądają

    

          
    
 void  loop  (  int  *  a  ,  int  n  )  {      #pragma cilk rozmiar ziarna = 100  // opcjonalnie 
        0      cilk_for  (  int  i  =  ;  i  <  n  ;  i  ++  )  { 
 a  [  i  ]  =  f  (  a  [  i  ]);  }  } 

To implementuje idiom mapy równoległej : treść pętli, tutaj wywołanie f , po którym następuje przypisanie do tablicy a , jest wykonywane dla każdej wartości i od zera do n w nieokreślonej kolejności. Opcjonalna pragma „rozmiaru ziarna” określa zgrubne: każda podtablica zawierająca sto lub mniej elementów jest przetwarzana sekwencyjnie. Chociaż specyfikacja Cilk nie określa dokładnego zachowania konstrukcji, typową implementacją jest rekurencja typu „dziel i zwyciężaj”, tak jakby programista napisał

       

            
                 
              
        
    
     
                 
           
          
        
    


    

     0 
 static  void  recursion  (  int  *  a  ,  int  start  ,  int  end  )  {  if  (  end  -  start  <=  100  )  {  // 100 tutaj to rozmiar ziarna.  for  (  int  ja  =  początek  ;  ja  <  koniec  ;  ja  ++  )  {  za  [  ja  ]  =  fa  (  za  [  ja  ]);  }  }  else  {  int  punkt środkowy  =  początek  +  (  koniec  -  początek  )  /  2  ;  rekurencja  cilk_spawn  (  a  ,  początek  ,  punkt środkowy  );  rekurencja  (  a  ,  punkt środkowy  ,  koniec  );  cilk_sync  ;  }  }  pusta  pętla  (  int  *  a  ,  int  n  )  {  rekurencja  (  a  ,  ,  n  );  } 

Powody generowania programu typu „dziel i zwyciężaj” zamiast oczywistej alternatywy, pętli, która odradza się i wywołuje ciało pętli jako funkcję, leżą zarówno w obsłudze rozmiaru ziarna, jak i wydajności: wykonanie całego tarła w jednym zadaniu powoduje obciążenie równoważenie wąskiego gardła.

Przegląd różnych konstrukcji pętli równoległych na HPCwire wykazał, że konstrukcja cilk_for jest dość ogólna, ale zauważono, że specyfikacja Cilk Plus nie określa, że ​​jej iteracje muszą być niezależne od danych, więc kompilator nie może automatycznie wektoryzować pętli cilk_for . W przeglądzie zwrócono również uwagę na fakt, że redukcje (np. sumy na tablicach) wymagają dodatkowego kodu.

Reduktory i hiperobiekty

Cilk++ dodał rodzaj obiektów zwanych hiperobiektami , które pozwalają wielu pasmom dzielić stan bez warunków wyścigu i bez używania jawnych blokad. Każda nić ma widok na hiperobiekt, którego może używać i aktualizować; podczas synchronizacji nici widoki są łączone w sposób określony przez programistę.

Najpopularniejszym typem hiperobiektu jest reduktor, który odpowiada klauzuli redukcji w OpenMP lub algebraicznemu pojęciu monoidu . Każdy reduktor ma element tożsamości i operację asocjacyjną , która łączy dwie wartości. Archetypowym reduktorem jest sumowanie liczb: elementem tożsamości jest zero, a operacja redukcji asocjacyjnej oblicza sumę. Ten reduktor jest wbudowany w Cilk++ i Cilk Plus:


 0
    0    
       // Oblicz ∑ foo(i) dla i od 0 do N, równolegle.  cilk  ::  reducer_opadd  <  float  >  wynik  (  );  cilk_for  (  int  i  =  ;  i  <  N  ;  i  ++  )  wynik  +=  foo  (  ja  ); 

Inne reduktory mogą być używane do konstruowania połączonych list lub łańcuchów, a programiści mogą definiować niestandardowe reduktory.

Ograniczeniem hiperobiektów jest to, że zapewniają one jedynie ograniczoną determinację . Burckhardta i in. zwróć uwagę, że nawet reduktor sumy może skutkować zachowaniem niedeterministycznym, pokazując program, który może generować 1 lub 2 w zależności od kolejności planowania:

     

 0
 
   0   

 void  add1  (  cilk  ::  reducer_opadd  <  int  >  &  r  )  {  r  ++  ;  }  // ...  cilk  ::  reducer_opadd  <  int  >  r  (  );  cilk_spawn  dodaj1  (  r  );  jeśli  (  r  ==  )  {  r  ++  ;  }  cilk_sync  ;  wyjście  (  r  .  get_value  ()); 

Notacja tablicowa

Intel Cilk Plus dodaje notację, aby wyrazić operacje wysokiego poziomu na całych tablicach lub sekcjach tablic ; np. axpy , która jest zwykle zapisywana

 
          
 
         0     
             
     
  // y ← α x + y  void  axpy  (  int  n  ,  float  alpha  ,  const  float  *  x  ,  float  *  y  )  {  for  (  int  i  =  ;  i  <  n  ;  i  ++  )  {  y  [  ja  ]  +=  alfa  *  x  [  ja  ];  }  } 

można w Cilk Plus wyrazić jako

y[0:n] += alfa * x[0:n];

Ta notacja pomaga kompilatorowi efektywnie wektoryzować aplikację. Intel Cilk Plus umożliwia równoległe wykonywanie operacji C/C++ na wielu elementach tablicy, a także zapewnia zestaw wbudowanych funkcji, których można używać do wykonywania wektorowych przesunięć, obrotów i redukcji. Podobna funkcjonalność istnieje w Fortranie 90 ; Cilk Plus różni się tym, że nigdy nie przydziela tymczasowych tablic, więc użycie pamięci jest łatwiejsze do przewidzenia.

Funkcje elementarne

W Cilk Plus funkcja elementarna jest zwykłą funkcją, którą można wywołać albo na argumentach skalarnych, albo równolegle na elementach tablicy. Są podobne do funkcji jądra OpenCL .

#pragma simd

Ta pragma daje kompilatorowi uprawnienia do wektoryzacji pętli nawet w przypadkach, w których automatyczna wektoryzacja może się nie powieść. Jest to najprostszy sposób na ręczne zastosowanie wektoryzacji.

Kradzież pracy

Harmonogram Cilk wykorzystuje politykę zwaną „kradzieżą pracy” w celu efektywnego podziału wykonywania procedur między wiele procesorów. Ponownie, najłatwiej jest to zrozumieć, jeśli najpierw przyjrzymy się, jak kod Cilk jest wykonywany na maszynie jednoprocesorowej.

Procesor utrzymuje stos , na którym umieszcza każdą ramkę, którą musi zawiesić, aby obsłużyć wywołanie procedury. Jeśli wykonuje fib(2) i napotka rekurencyjne wywołanie fib(1) , zapisze stan fib(2) , w tym jego zmienne i miejsce, w którym kod zawiesił wykonywanie, i umieści ten stan na stosie. Nie usunie stanu zawieszenia ze stosu i nie wznowi wykonywania, dopóki wywołanie procedury, które spowodowało zawieszenie, oraz wszystkie procedury wywołane kolejno przez tę procedurę nie zostaną w pełni wykonane.

W przypadku wielu procesorów rzeczy oczywiście się zmieniają. Każdy procesor nadal ma stos do przechowywania ramek, których wykonanie zostało zawieszone; jednak te stosy bardziej przypominają deques , ponieważ stany zawieszenia można usunąć z dowolnego końca. Procesor nadal może usuwać stany ze swojego stosu tylko z tego samego końca, na którym je umieszcza; jednak każdy procesor, który obecnie nie działa (zakończył swoją własną pracę lub nie został jeszcze przydzielony), wybierze losowo inny procesor za pośrednictwem programu planującego i spróbuje „ukraść” pracę z przeciwnego końca ich stosu - stany zawieszenia, które kradnący procesor może następnie rozpocząć wykonywanie. Stany, które zostały skradzione, to stany, z których skradziony procesor mógłby wykonać się jako ostatni.

Zobacz też

Linki zewnętrzne