Cilk
Cilk
Main page

Cilk

logo
Community Hub0 subscribers
Read side by side
from Wikipedia
Cilk
Paradigmimperative (procedural), structured, parallel
Designed byMIT Laboratory for Computer Science
DeveloperIntel
First appeared1994
Typing disciplinestatic, weak, manifest
Websitecilk.mit.edu
Dialects
Cilk++, Cilk Plus, OpenCilk
Influenced by
C
Influenced
OpenMP 3.0,[1] Rayon (Rust library)[2]
OpenCilk
Designed byMIT
DeveloperMIT
First appeared2020
Stable release
2.0.1 / September 3, 2022; 3 years ago (2022-09-03)
OSUnix-like, macOS
LicenseMIT
Websitewww.opencilk.org
Cilk Plus
Designed byIntel
DeveloperIntel
First appeared2010
Stable release
1.2 / September 9, 2013; 12 years ago (2013-09-09)
Filename extensions(Same as C or C++)
Websitehttp://cilkplus.org/

Cilk, Cilk++, Cilk Plus and OpenCilk are general-purpose programming languages designed for multithreaded parallel computing. They are based on the C and C++ programming languages, which they extend with constructs to express parallel loops and the fork–join idiom.

Originally developed in the 1990s at the Massachusetts Institute of Technology (MIT) in the group of Charles E. Leiserson, Cilk was later commercialized as Cilk++ by a spinoff company, Cilk Arts. That company was subsequently acquired by Intel, which increased compatibility with existing C and C++ code, calling the result Cilk Plus. After Intel stopped supporting Cilk Plus in 2017, MIT is again developing Cilk in the form of OpenCilk.

History

[edit]

MIT Cilk

[edit]

The Cilk programming language grew out of three separate projects at the MIT Laboratory for Computer Science:[3]

  • Theoretical work on scheduling multi-threaded applications.
  • StarTech – a parallel chess program built to run on the Thinking Machines Corporation's Connection Machine model CM-5.
  • PCM/Threaded-C – a C-based package for scheduling continuation-passing-style threads on the CM-5

In April 1994 the three projects were combined and christened "Cilk". The name Cilk is not an acronym, but an allusion to "nice threads" (silk) and the C programming language. The Cilk-1 compiler was released in September 1994.

The original Cilk language was based on ANSI C, with the addition of Cilk-specific keywords to signal parallelism. When the Cilk keywords are removed from Cilk source code, the result should always be a valid C program, called the serial elision (or C elision) of the full Cilk program, with the same semantics as the Cilk program running on a single processor. Despite several similarities,[which?] Cilk is not directly related to AT&T Bell Labs' Concurrent C.

Cilk was implemented as a translator to C, targeting the GNU C Compiler (GCC). The last version, Cilk 5.4.6, is available from the MIT Computer Science and Artificial Intelligence Laboratory (CSAIL), but is no longer supported.[4]

A showcase for Cilk's capabilities was the Cilkchess parallel chess-playing program, which won several computer chess prizes in the 1990s, including the 1996 Open Dutch Computer Chess Championship.[5]

Cilk Arts and Cilk++

[edit]

Prior to c. 2006, the market for Cilk was restricted to high-performance computing. The emergence of multicore processors in mainstream computing meant that hundreds of millions of new parallel computers were being shipped every year. Cilk Arts was formed to capitalize on that opportunity: in 2006, Leiserson launched Cilk Arts to create and bring to market a modern version of Cilk that supports the commercial needs of an upcoming generation of programmers. The company closed a Series A venture financing round in October 2007, and its product, Cilk++ 1.0, shipped in December, 2008.

Cilk++ differs from Cilk in several ways: support for C++, support for loops, and hyperobjects – a new construct designed to solve data race problems created by parallel accesses to global variables. Cilk++ was proprietary software. Like its predecessor, it was implemented as a Cilk-to-C++ compiler. It supported the Microsoft and GNU compilers.

Intel Cilk Plus

[edit]

On July 31, 2009, Cilk Arts announced on its web site that its products and engineering team were now part of Intel Corp. In early 2010, the Cilk website at www.cilk.com began redirecting to the Intel website (as of early 2017, the original Cilk website no longer resolves to a host). Intel and Cilk Arts integrated and advanced the technology further resulting in a September 2010 release of Intel Cilk Plus.[6][7] Cilk Plus adopts simplifications, proposed by Cilk Arts in Cilk++, to eliminate the need for several of the original Cilk keywords while adding the ability to spawn functions and to deal with variables involved in reduction operations. Cilk Plus differs from Cilk and Cilk++ by adding array extensions, being incorporated in a commercial compiler (from Intel), and compatibility with existing debuggers.[8]

Cilk Plus was first implemented in the Intel C++ Compiler with the release of the Intel compiler in Intel Composer XE 2010.[citation needed] An open source (BSD-licensed) implementation was contributed by Intel to the GNU Compiler Collection (GCC), which shipped Cilk Plus support in version 4.9,[9] except for the _Cilk_for keyword, which was added in GCC 5.0. In February 2013, Intel announced a Clang fork with Cilk Plus support.[10] The Intel Compiler, but not the open source implementations, comes with a race detector and a performance analyzer.

Intel later discontinued it, recommending its users switch to instead using either OpenMP or Intel's own TBB library for their parallel programming needs.[11]

Differences between versions

[edit]

In the original MIT Cilk implementation, the first Cilk keyword is in fact cilk, which identifies a function which is written in Cilk. Since Cilk procedures can call C procedures directly, but C procedures cannot directly call or spawn Cilk procedures, this keyword is needed to distinguish Cilk code from C code. Cilk Plus removes this restriction, as well as the cilk keyword, so C and C++ functions can call into Cilk Plus code and vice versa.

Deprecation of Cilk Plus

[edit]

In May, 2017, GCC 7.1 was released and marked Cilk Plus support as deprecated.[12] Intel itself announced in September 2017 that they would deprecate Cilk Plus with the 2018 release of the Intel Software Development Tools.[11] In May 2018, GCC 8.1 was released with Cilk Plus support removed.[13]

OpenCilk

[edit]

After Cilk Plus support was deprecated by Intel, MIT has taken on the development of Cilk in the OpenCilk implementation, focusing on the LLVM/Clang fork now termed "Tapir".[11][14] OpenCilk remains largely compatible with Intel Cilk Plus.[15] Its first stable version was released in March 2021.[16]

Language features

[edit]

The principle behind the design of the Cilk language is that the programmer should be responsible for exposing the parallelism, identifying elements that can safely be executed in parallel; it should then be left to the run-time environment, particularly the scheduler, to decide during execution how to actually divide the work between processors. It is because these responsibilities are separated that a Cilk program can run without rewriting on any number of processors, including one.

Task parallelism: spawn and sync

[edit]

Cilk's main addition to C are two keywords that together allow writing task-parallel programs.

  • The spawn keyword, when preceding a function call (spawn f(x)), indicates that the function call (f(x)) can safely run in parallel with the statements following it in the calling function. Note that the scheduler is not obligated to run this procedure in parallel; the keyword merely alerts the scheduler that it can do so.
  • A sync statement indicates that execution of the current function cannot proceed until all previously spawned function calls have completed. This is an example of a barrier method.

(In Cilk Plus, the keywords are spelled _Cilk_spawn and _Cilk_sync, or cilk_spawn and cilk_sync if the Cilk Plus headers are included.)

Below is a recursive implementation of the Fibonacci function in Cilk, with parallel recursive calls, which demonstrates the spawn, and sync keywords. The original Cilk required any function using these to be annotated with the cilk keyword, which is gone as of Cilk Plus. (Cilk program code is not numbered; the numbers have been added only to make the discussion easier to follow.)

cilk int fib(int n) {
    if (n < 2) {
        return n;
    }
    else {
       int x, y;

       x = spawn fib(n - 1);
       y = spawn fib(n - 2);

       sync;

       return x + y;
    }
}

If this code was executed by a single processor to determine the value of fib(2), that processor would create a frame for fib(2), and execute lines 1 through 5. On line 6, it would create spaces in the frame to hold the values of x and y. On line 8, the processor would have to suspend the current frame, create a new frame to execute the procedure fib(1), execute the code of that frame until reaching a return statement, and then resume the fib(2) frame with the value of fib(1) placed into fib(2)'s x variable. On the next line, it would need to suspend again to execute fib(0) and place the result in fib(2)'s y variable.

When the code is executed on a multiprocessor machine, however, execution proceeds differently. One processor starts the execution of fib(2); when it reaches line 8, however, the spawn keyword modifying the call to fib(n-1) tells the processor that it can safely give the job to a second processor: this second processor can create a frame for fib(1), execute its code, and store its result in fib(2)'s frame when it finishes; the first processor continues executing the code of fib(2) at the same time. A processor is not obligated to assign a spawned procedure elsewhere; if the machine only has two processors and the second is still busy on fib(1) when the processor executing fib(2) gets to the procedure call, the first processor will suspend fib(2) and execute fib(0) itself, as it would if it were the only processor. Of course, if another processor is available, then it will be called into service, and all three processors would be executing separate frames simultaneously.

(The preceding description is not entirely accurate. Even though the common terminology for discussing Cilk refers to processors making the decision to spawn off work to other processors, it is actually the scheduler which assigns procedures to processors for execution, using a policy called work-stealing, described later.)

If the processor executing fib(2) were to execute line 13 before both of the other processors had completed their frames, it would generate an incorrect result or an error; fib(2) would be trying to add the values stored in x and y, but one or both of those values would be missing. This is the purpose of the sync keyword, which we see in line 11: it tells the processor executing a frame that it must suspend its own execution until all the procedure calls it has spawned off have returned. When fib(2) is allowed to proceed past the sync statement in line 11, it can only be because fib(1) and fib(0) have completed and placed their results in x and y, making it safe to perform calculations on those results.

The code example above uses the syntax of Cilk-5. The original Cilk (Cilk-1) used a rather different syntax that required programming in an explicit continuation-passing style, and the Fibonacci examples looks as follows:[17]

thread fib(cont int k, int n)
{
    if (n < 2) {
        send_argument(k, n);
    }
    else {
        cont int x, y;
        spawn_next sum(k, ?x, ?y);
        spawn fib(x, n - 1);
        spawn fib(y, n - 2);
    }
}

thread sum(cont int k, int x, int y)
{
     send_argument(k, x + y);
}

Inside fib's recursive case, the spawn_next keyword indicates the creation of a successor thread (as opposed to the child threads created by spawn), which executes the sum subroutine after waiting for the continuation variables x and y to be filled in by the recursive calls. The base case and sum use a send_argument(k, n) operation to set their continuation variable k to the value of n, effectively "returning" the value to the successor thread.

Inlets

[edit]

The two remaining Cilk keywords are slightly more advanced, and concern the use of inlets. Ordinarily, when a Cilk procedure is spawned, it can return its results to the parent procedure only by putting those results in a variable in the parent's frame, as we assigned the results of our spawned procedure calls in the example to x and y.

The alternative is to use an inlet. An inlet is a function internal to a Cilk procedure which handles the results of a spawned procedure call as they return. One major reason to use inlets is that all the inlets of a procedure are guaranteed to operate atomically with regards to each other and to the parent procedure, thus avoiding the bugs that could occur if the multiple returning procedures tried to update the same variables in the parent frame at the same time.

  • The inlet keyword identifies a function defined within the procedure as an inlet.
  • The abort keyword can only be used inside an inlet; it tells the scheduler that any other procedures that have been spawned off by the parent procedure can safely be aborted.

Inlets were removed when Cilk became Cilk++, and are not present in Cilk Plus.

Parallel loops

[edit]

Cilk++ added an additional construct, the parallel loop, denoted cilk_for in Cilk Plus. These loops look like

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

This implements the parallel map idiom: the body of the loop, here a call to f followed by an assignment to the array a, is executed for each value of i from zero to n in an indeterminate order. The optional "grain size" pragma determines the coarsening: any sub-array of one hundred or fewer elements is processed sequentially. Although the Cilk specification does not specify the exact behavior of the construct, the typical implementation is a divide-and-conquer recursion,[18] as if the programmer had written

static void recursion(int *a, int start, int end)
{
    if (end - start <= 100) {  // The 100 here is the grainsize.
        for (int i = start; i < end; i++) {
            a[i] = f(a[i]);
        }
    }
    else {
        int midpoint = start + (end - start) / 2;
        cilk_spawn recursion(a, start, midpoint);
        recursion(a, midpoint, end);
        cilk_sync;
    }
}

void loop(int *a, int n)
{
    recursion(a, 0, n);
}

The reasons for generating a divide-and-conquer program rather than the obvious alternative, a loop that spawn-calls the loop body as a function, lie in both the grainsize handling and in efficiency: doing all the spawning in a single task makes load balancing a bottleneck.[19]

A review of various parallel loop constructs on HPCwire found the cilk_for construct to be quite general, but noted that the Cilk Plus specification did not stipulate that its iterations need to be data-independent, so a compiler cannot automatically vectorize a cilk_for loop. The review also noted the fact that reductions (e.g., sums over arrays) need additional code.[18]

Reducers and hyperobjects

[edit]

Cilk++ added a kind of objects called hyperobjects, that allow multiple strands to share state without race conditions and without using explicit locks. Each strand has a view on the hyperobject that it can use and update; when the strands synchronize, the views are combined in a way specified by the programmer.[20]

The most common type of hyperobject is a reducer, which corresponds to the reduction clause in OpenMP or to the algebraic notion of a monoid. Each reducer has an identity element and an associative operation that combines two values. The archetypal reducer is summation of numbers: the identity element is zero, and the associative reduce operation computes a sum. This reducer is built into Cilk++ and Cilk Plus:

// Compute ∑ foo(i) for i from 0 to N, in parallel.
cilk::reducer_opadd<float> result(0);
cilk_for (int i = 0; i < N; i++)
    result += foo(i);

Other reducers can be used to construct linked lists or strings, and programmers can define custom reducers.

A limitation of hyperobjects is that they provide only limited determinacy. Burckhardt et al. point out that even the sum reducer can result in non-deterministic behavior, showing a program that may produce either 1 or 2 depending on the scheduling order:[21]

void add1(cilk::reducer_opadd<int> &r) { r++; }
// ...
cilk::reducer_opadd<int> r(0);
cilk_spawn add1(r);
if (r == 0) { r++; }
cilk_sync;
output(r.get_value());

Array notation

[edit]

Intel Cilk Plus adds notation to express high-level operations on entire arrays or sections of arrays; e.g., an axpy-style function that is ordinarily written

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

can in Cilk Plus be expressed as

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

This notation helps the compiler to effectively vectorize the application. Intel Cilk Plus allows C/C++ operations to be applied to multiple array elements in parallel, and also provides a set of built-in functions that can be used to perform vectorized shifts, rotates, and reductions. Similar functionality exists in Fortran 90; Cilk Plus differs in that it never allocates temporary arrays, so memory usage is easier to predict.

Elemental functions

[edit]

In Cilk Plus, an elemental function is a regular function which can be invoked either on scalar arguments or on array elements in parallel. They are similar to the kernel functions of OpenCL.

#pragma simd

[edit]

This pragma gives the compiler permission to vectorize a loop even in cases where auto-vectorization might fail. It is the simplest way to manually apply vectorization.

Work-stealing

[edit]

The Cilk scheduler uses a policy called "work-stealing" to divide procedure execution efficiently among multiple processors. Again, it is easiest to understand if we look first at how Cilk code is executed on a single-processor machine.

The processor maintains a stack on which it places each frame that it has to suspend in order to handle a procedure call. If it is executing fib(2), and encounters a recursive call to fib(1), it will save fib(2)'s state, including its variables and where the code suspended execution, and put that state on the stack. It will not take a suspended state off the stack and resume execution until the procedure call that caused the suspension, and any procedures called in turn by that procedure, have all been fully executed.

With multiple processors, things of course change. Each processor still has a stack for storing frames whose execution has been suspended; however, these stacks are more like deques, in that suspended states can be removed from either end. A processor can still only remove states from its own stack from the same end that it puts them on; however, any processor which is not currently working (having finished its own work, or not yet having been assigned any) will pick another processor at random, through the scheduler, and try to "steal" work from the opposite end of their stack – suspended states, which the stealing processor can then begin to execute. The states which get stolen are the states that the processor stolen from would get around to executing last.

See also

[edit]

References

[edit]
[edit]
Revisions and contributorsEdit on WikipediaRead on Wikipedia
from Grokipedia
Cilk is a multithreaded programming language designed for parallel computing, originally developed at the Massachusetts Institute of Technology (MIT) in 1994 as a simple extension to the C language that enables programmers to express task-parallel algorithms using a small set of linguistic primitives, such as the cilk_spawn and cilk_sync keywords, while relying on an efficient work-stealing runtime scheduler to manage task distribution across multiple processors.[1][2] The core programming model follows a fork-join paradigm, where functions can spawn parallel child tasks that execute asynchronously and join back upon synchronization, ensuring deterministic execution and composability for nested parallelism without explicit thread management.[3] This approach abstracts away low-level details of concurrency, allowing developers to focus on algorithmic structure while achieving high performance on multicore systems through provably efficient scheduling that minimizes work overhead and balances load.[2] Key innovations in Cilk include its serial elision property, which guarantees that serial C++ code runs unchanged and with identical semantics under the parallel runtime, and support for parallel reductions via reducer hyperobjects to handle associative operations like summation or minimization in parallel loops.[4] The language evolved through several versions: the initial Cilk system targeted shared-memory multiprocessors and was implemented on architectures like the Connection Machine CM-5, demonstrating applications in protein folding, graphics rendering, and chess programming.[5] In 2006, MIT licensed the technology to Cilk Arts, Inc., founded by key developers Charles E. Leiserson and Matteo Frigo, leading to Cilk++, an extension for C++ that integrated seamlessly with object-oriented programming.[1] Intel acquired Cilk Arts in 2009, rebranding it as Intel Cilk Plus, which added SIMD vectorization directives (_Cilk_simd) and was incorporated into Intel's commercial compilers as well as open-source GCC and Clang/LLVM until its deprecation in 2018.[1] Today, OpenCilk represents the active, open-source continuation of the project at MIT, with version 3.0 released in May 2025, featuring a modular architecture with an LLVM-based compiler, a user-mode runtime library, and tools like Cilkscale for measuring parallelism via work and span metrics.[6] This infrastructure supports experimentation with custom languages and runtimes, emphasizing processor-oblivious programming that scales efficiently without tuning for specific hardware.[7] Cilk's influence extends to modern parallel frameworks, underscoring its role in advancing lightweight, high-performance task parallelism for scientific computing and beyond.[8]

Overview

Definition and Scope

Cilk is a family of programming languages designed as linguistic extensions to C and C++ for expressing multithreaded parallel computations through task parallelism, enabling developers to create efficient parallel programs without managing low-level threading details.[9][10] The family encompasses several iterations: MIT Cilk, an extension to the C language for parallel programming on shared-memory systems; Cilk++, an open-source extension to C++ that supports scalable parallelism; Intel Cilk Plus, an extension to C and C++ incorporating task parallelism alongside data-parallel features like vectorization; and OpenCilk, an open-source platform extending C and C++ to simplify task-parallel programming. OpenCilk 3.0, released in May 2025, introduced the cilk_scope keyword and enhanced integration with IDEs like VS Code.[11][1] Each version maintains compatibility with standard C and C++ syntax while adding minimal keywords to denote parallelism.[4] The scope of Cilk centers on shared-memory multiprocessors, where it facilitates the development of portable parallel applications that scale with available processors.[12] It ensures ANSI C compatibility, allowing seamless integration of parallel code with existing sequential libraries, and promotes deterministic execution by encouraging a race-free programming model that avoids data races through structured task dependencies.[10][13] This focus distinguishes Cilk from distributed-memory paradigms, emphasizing lightweight, dynamic scheduling for irregular workloads.[4] In contrast to low-level libraries like pthreads, which require explicit thread creation, synchronization, and load balancing, Cilk employs high-level abstractions to automatically handle these aspects, reducing programming complexity.[9] Similarly, while sharing some directive-based similarities with OpenMP for loop parallelism, Cilk prioritizes fine-grained task parallelism for non-iterative, recursive, or dynamic computations, offering greater expressiveness without manual thread management.[14][4]

Design Principles

Cilk's design emphasizes linguistic simplicity by extending C and C++ with a minimal set of keywords—such as cilk, spawn, and sync in the original version, or cilk_spawn and cilk_sync in later versions—to enable multithreaded parallelism without requiring programmers to manage low-level threading details such as thread creation, synchronization primitives, or load balancing.[15] This approach allows Cilk programs to elide parallel keywords and compile to valid, semantically equivalent serial C or C++ code, ensuring ease of development and debugging while leveraging existing C and C++ compilers and tools.[15] A core efficiency goal of Cilk is to achieve asymptotically optimal scheduling on parallel machines, guided by the work-depth model of multithreaded computation. In this model, the total execution time $ T_P $ on $ P $ processors is bounded by $ T_P = O\left( \frac{T_1}{P} + T_\infty \right) $, where $ T_1 $ represents the total computational work (as in a serial execution) and $ T_\infty $ is the critical-path length or depth (the longest chain of dependent tasks).[16] This formulation ensures near-linear speedup for balanced workloads, with the runtime system handling dynamic load balancing to minimize idle time.[16] Cilk promotes determinism and race-freedom by structuring parallelism such that programs execute identically to their serial elided versions, avoiding nondeterministic outcomes from race conditions.[15] The language's fully strict semantics restrict thread interactions to well-defined parent-child relationships in the computation dag, preventing data races without explicit locks in many cases.[16] The theoretical foundation of Cilk derives from the multithreaded computation model developed by Blumofe and Leiserson, which formalizes computations as directed acyclic graphs of tasks with dependencies.[16] This model supports provably efficient randomized work-stealing scheduling, where idle processors steal tasks from randomly selected peers to achieve load balance, guaranteeing expected space usage of $ O(S_1) $ (where $ S_1 $ is serial space) and time within the work-depth bound.[16]

Historical Development

MIT Cilk (1994–2006)

The development of Cilk began in 1994 at the MIT Laboratory for Computer Science, led by Charles E. Leiserson in collaboration with a team including Robert D. Blumofe, Christopher F. Joerg, Bradley C. Kuszmaul, Keith H. Randall, and others.[17] This project originated as an extension to ANSI C, designed specifically to enable efficient shared-memory parallelism on multiprocessor systems, building on earlier influences like Michael Halbherr's PCM system.[2] The initial implementation, known as Cilk-1, targeted architectures such as the Connection Machine CM-5 and introduced core mechanisms for multithreaded execution without requiring programmers to manage low-level thread details.[5] A primary innovation in this early phase was the introduction of the spawn and sync keywords, which facilitated dynamic task parallelism. The spawn keyword allowed a procedure to invoke a child procedure concurrently as a new thread, while sync acted as a local barrier, ensuring all spawned children completed before proceeding, thus structuring computations as serialized parallel procedures. Complementing this, the inlet construct provided a mechanism for handling the completion of spawned functions, enabling nondeterministic selection of return values in a structured way, such as atomically processing the first available result from parallel branches.[12] These features emphasized a "work-first" principle, prioritizing serial execution efficiency while enabling scalable parallelism. The academic contributions of MIT Cilk during this period centered on scheduling research, particularly the development and analysis of work-stealing algorithms for load balancing. In 1994, Blumofe and Leiserson published foundational work proving that a randomized work-stealing scheduler could execute fully strict multithreaded computations with expected linear time overhead relative to work and span, minimizing idle time across processors.[17] This was extended in subsequent publications, including a 1995 PPoPP paper documenting the runtime system's efficiency on platforms like the Intel Paragon and Sun SPARC SMPs, where Cilk achieved near-ideal speedup for applications such as protein folding and computer chess.[2] Further refinements appeared in 1998, with analyses showing spawn overhead reduced to less than three times a standard C function call, enhancing portability and performance. Key milestones marked the evolution toward stability and broader adoption. Cilk 5.4, released in 1997 as a stable version, incorporated these optimizations and served as a reference implementation for academic use, supporting applications in backtrack search and graphics rendering.[18] By the early 2000s, enhancements in Cilk-5 versions improved cross-platform support, culminating in integration with POSIX threads (pthreads) around 2003 to enhance portability on Unix-like systems without relying on custom threading models.[12] This pthread compatibility allowed Cilk workers to leverage OS-level threading, broadening its applicability in research environments up to 2006.[19]

Cilk Arts and Cilk++ (2006–2009)

In 2006, Cilk Arts, Inc. was founded as a spin-off from the Massachusetts Institute of Technology (MIT) by researchers including Charles E. Leiserson and Matteo Frigo to commercialize the Cilk parallel programming technology developed at MIT.[20] The company aimed to address the growing demand for parallel programming tools amid the shift to multicore processors in the mid-2000s, licensing the core Cilk innovations from MIT to build a commercial implementation.[21] As a venture-funded startup, Cilk Arts raised capital to support its development efforts, targeting software developers seeking efficient ways to exploit parallelism on emerging hardware.[22] Cilk Arts released Cilk++ in 2007 as an extension to C++, providing full support for object-oriented programming features such as classes, inheritance, and exceptions while preserving the serial semantics of the original MIT Cilk.[23] This version maintained backward compatibility with MIT Cilk code through the use of keywords like cilk_spawn for spawning parallel tasks and cilk_sync for synchronization, allowing existing Cilk programs to run unchanged under Cilk++. The enhancements included an improved compiler built on the GNU Compiler Collection (GCC) backend, which offered better performance and portability, along with seamless integration with standard C++ libraries such as <algorithm> and <iterator>.[23] These improvements enabled developers to write portable, high-performance parallel code without sacrificing the simplicity of the original model. Despite its innovations, Cilk Arts faced challenges in the competitive market for parallel programming tools during a period of rapid hardware evolution and limited adoption of new languages. The company shipped Cilk++ 1.0 in December 2008, but its independent operations ended shortly thereafter. In July 2009, Intel Corporation acquired Cilk Arts, incorporating its technology, products, and engineering team into Intel's portfolio to further advance parallel computing solutions.[22] This acquisition marked the transition of Cilk++ development from a startup to integration within a major industry player.[24]

Intel Cilk Plus (2009–2017)

In 2009, Intel Corporation acquired Cilk Arts, the company that had commercialized Cilk as Cilk++ , and integrated the technology into its product lineup, renaming it Intel Cilk Plus to emphasize its expanded capabilities.[1][21] This acquisition allowed Intel to merge Cilk's task parallelism features with its own vectorization technologies, aiming to simplify parallel programming for both multithreaded and SIMD execution on Intel hardware.[21] Intel Cilk Plus was first released as part of the Intel Composer XE 2010 suite, bundled with the Intel C++ Compiler (ICC), providing developers with a production-ready extension for C and C++.[25] Key enhancements in Intel Cilk Plus included array notation, which enabled concise expression of vector operations on entire array sections, such as C[0:length] = A[0:length] + B[0:length];, to facilitate automatic SIMD vectorization by the compiler.[26] Elemental functions complemented this by applying scalar operations element-wise across arrays, while the #pragma simd directive allowed explicit hints for loop vectorization, improving performance in data-parallel workloads without altering core algorithms.[27][21] These features built on prior Cilk variants by extending support for reducers—specialized data structures for race-free parallel reductions—while shifting focus toward hardware-optimized data parallelism.[28] Intel Cilk Plus was primarily supported through the Intel C++ Compiler (ICC), starting with version 12.0 in 2010, where it became a core extension for parallel code generation on x86 architectures.[29] Open-source integration followed in 2014 with GCC 4.9, enabling the -fcilkplus flag for broader adoption across Linux environments and non-Intel compilers.[30] This compiler support facilitated its use until 2017, when Intel began phasing it out in favor of other parallelism models. During its active period, Intel Cilk Plus saw adoption in high-performance computing (HPC) applications, particularly scientific simulations requiring both task and data parallelism. For instance, it was employed in Monte Carlo simulations to accelerate vectorized computations on multicore processors, achieving scalable performance without manual thread management.[28] Similarly, benchmarks in computational epidemiology used Cilk Plus for parallelizing epidemic modeling simulations, demonstrating speedups on Intel architectures for large-scale parameter sweeps.[31] Unlike earlier Cilk versions focused solely on task parallelism, Intel Cilk Plus differentiated itself by incorporating SIMD-based data parallelism, enabling finer-grained exploitation of vector units in modern CPUs for HPC workloads.[21]

OpenCilk and Beyond (2018–present)

In 2017, Intel announced the deprecation of Cilk Plus within the Intel C++ Compiler starting with the 2018 release, initiating a two-year period before full end-of-life support by 2020, and advising users to transition to alternatives such as OpenMP or Intel Threading Building Blocks.[32] This decision prompted the need for an open-source successor to preserve the Cilk programming model. In 2018, the Massachusetts Institute of Technology (MIT) launched the OpenCilk project as an open-source platform to sustain and evolve Cilk's task-parallel programming capabilities, forking core technology from Intel Cilk Plus for ongoing maintenance and research accessibility.[33] OpenCilk emphasizes modularity, allowing reuse of components like the Tapir/LLVM compiler infrastructure and the Cheetah runtime system.[7] Significant milestones followed, including integration with the LLVM/Clang compiler framework around 2020, which enables Cilk program compilation using the -fopencilk flag and leverages LLVM's optimizations for high-performance code generation.[7] This integration also provides support for modern C++ standards, aligning with all features available in LLVM versions up to 19.[34] Recent advancements culminated in the May 2025 release of OpenCilk 3.0, which introduced beta support for range cilk_for loops over random-access containers like std::vector, enabling concise parallel iteration such as cilk_for (auto x : v) do_stuff(x);; beta compatibility for Android applications to facilitate task-parallel C/C++ development on mobile platforms; enhanced reducers for C++ structs and classes as hyperobjects; and integration with VS Code via a custom clangd extension for improved syntax highlighting and editing of Cilk keywords like cilk_spawn and cilk_sync.[11] The release also upgraded to LLVM 19.1.7 for better performance and removed the requirement for the proprietary cilk/cilk.h header, simplifying setup.[11] As of 2025, OpenCilk sustains an active development community within the Fastcode open-source ecosystem, prioritizing cross-platform portability—including x86, ARM, and now Android—and user-friendly adoption without reliance on vendor-specific tools.[35][34] OpenCilk maintains broad compatibility with Intel Cilk Plus features, such as array notation, though some elements like array-slice notation remain unsupported.[34]

Programming Model

Task Parallelism: spawn and sync

Task parallelism in Cilk is primarily achieved through the spawn and sync keywords, which enable the creation and synchronization of parallel tasks in a lightweight manner. The spawn keyword marks a function call as a potential child task, allowing the parent function to continue execution immediately without waiting for the child to complete, thereby forking the control flow into parallel strands.[36] This mechanism assumes familiarity with standard C or C++ function calls and introduces a dominance relation where the spawned child task is sequenced after the spawn point but executes concurrently and unsequenced with the parent's continuation strand. (Note: Using the correct URL if available; assuming seminal paper.) The sync keyword, conversely, suspends the execution of the current function until all previously spawned child tasks within the same scope have completed, ensuring that dependencies are resolved before proceeding.[36] This creates a join point in the computation, merging the parallel strands back into a single execution path, with the post-sync strand sequenced after all child strands.[37] Together, spawn and sync form the basis of Cilk's fork-join model, structuring programs as directed acyclic graphs (DAGs) of tasks where dominance enforces ordering constraints. A classic example of task parallelism using these keywords is the recursive computation of the Fibonacci sequence, which demonstrates nested parallelism in a divide-and-conquer pattern. The Cilk code for this is:
cilk int fib(int n) {
    if (n < 2) return n;
    int x = spawn fib(n-1);
    int y = spawn fib(n-2);
    sync;
    return x + y;
}
Here, the two recursive calls are spawned as independent child tasks that may execute in parallel, and the sync ensures their results are available before summation.[37] This approach exploits the inherent independence of the subproblems, scaling with available processors while maintaining a simple, recursive structure akin to serial code. Cilk supports serial elision, meaning the spawn and sync keywords are ignored during serial execution, producing identical output to the parallel version without any runtime overhead from parallelism.[36] This feature ensures that Cilk programs are deterministic and easily debuggable in a single-threaded environment, as the dominance relations and task dependencies are preserved sequentially.

Inlets and Synchronization

In Cilk, inlets provide a mechanism for handling the completion of individual spawned child procedures asynchronously, without requiring a full synchronization of all children via the sync statement. An inlet is a special function defined within a parent Cilk procedure that is invoked automatically upon the return of a specific spawned child, allowing the parent to process the child's result immediately while other siblings continue executing. This enables fine-grained control over asynchronous returns, facilitating patterns like incremental result aggregation or early termination decisions before the entire spawn-sync block completes.[15] The syntax for defining an inlet in early versions such as Cilk-5 involves declaring it as a nested function within the parent procedure using the inlet keyword, followed by the return type, name, and parameters. For example: inlet void summer(int result) { x += result; }. The inlet is then invoked by associating it with a spawn, such as summer(spawn fib(n-1));, where the spawned procedure's return value is passed directly to the inlet's parameter. The inlet executes atomically on the parent's processor upon the child's completion, and control in the parent resumes immediately after the inlet call, independent of other ongoing spawns. Inlets may contain arbitrary Cilk code but are restricted from executing sync statements themselves and, in Cilk-5, can handle only a single spawned argument in the first position.[15] Inlets are particularly useful for applications requiring partial result processing from parallel subtasks, such as aggregating values from children without blocking on a global sync. A representative use case is in a parallel Fibonacci computation, where inlets sum results from spawned recursive calls incrementally: the parent spawns multiple fib computations, and each completing child triggers an inlet to update a running total, allowing the parent to proceed or make decisions (e.g., via aborts) based on partial outcomes. Similarly, in a parallel mergesort, inlets could compute partial sums or statistics from sorted subarrays as they complete, enabling optimizations like early pruning without awaiting full synchronization of all partitions. This approach supports speculative parallelism, as seen in applications like chess search engines, where an inlet might evaluate a child's result to abort inferior branches.[15][38] Support for inlets originated in MIT Cilk versions starting with Cilk-4 and was fully implemented in Cilk-5, where they addressed limitations in handling dynamic, speculative workloads. In Cilk Arts' Cilk++ (2006–2009), native inlet support was not provided due to C++'s lack of nested functions, leading to library-based workarounds using lambdas or polling for similar asynchronous handling. However, inlets were deprecated and removed in Intel Cilk Plus (2009–2017), with the language simplifying toward reducers and explicit sync for result aggregation to improve portability and reduce complexity. OpenCilk (2018–present) continues this trend by omitting inlets entirely, favoring the core spawn-sync model augmented by hyperobjects for advanced synchronization needs.[15][39][40]

Parallel Loops

In Cilk, parallel loops are implemented using the cilk_for keyword, which replaces the standard for loop to allow iterations to execute concurrently across multiple worker threads. The syntax is identical to a conventional C or C++ for loop, such as cilk_for (int i = 0; i < N; ++i) { body; }, where the body of each iteration is treated as an independent task that can run in parallel without requiring explicit synchronization among iterations, provided there are no data dependencies. This construct was introduced in Cilk-5, released in 1997 by researchers at MIT, as a high-level abstraction for data parallelism built upon the underlying task parallelism model of spawn and sync.[3] The Cilk runtime system achieves load balancing for cilk_for loops through a work-stealing scheduler that dynamically partitions the iteration space into subtasks, recursively subdividing ranges as needed to distribute work across processors. Unlike static chunking approaches in languages like OpenMP, which divide the loop into fixed-sized blocks at compile time, Cilk's dynamic splitting ensures adaptive granularity: the runtime initially assigns larger chunks to threads and allows idle workers to steal smaller subranges from busy ones, minimizing overhead while maintaining balance even for irregular workloads. Grain size, which controls the minimum task size to amortize scheduling costs, can be tuned using the #pragma cilk grainsize directive, with a default often set to approximately the loop range divided by eight times the number of processors for efficient parallelism. This mechanism, proven to achieve near-optimal speedup in terms of work and span, was a core innovation in the original MIT Cilk implementation.[3][4] OpenCilk, the modern open-source continuation of the MIT Cilk project, enhances cilk_for support starting from version 3.0 (released in May 2025) with beta-range-based loops compatible with C++11 and later standards. These allow parallel iteration over random-access containers, such as cilk_for (auto& elem : std::vector<int>{...}) { body; }, enabling concise parallelization of algorithms on STL collections without manual index management, while preserving the dynamic load balancing of traditional cilk_for. This extension builds on the serial elision principle, ensuring the code remains deterministic and efficient when run serially by eliding parallelism.[11][4] A representative example of cilk_for usage is in parallel matrix multiplication for an n×nn \times n product C=A×BC = A \times B, where the outer loop over rows can be parallelized to distribute independent row computations across threads:
void matmul(double A[n][n], double B[n][n], double C[n][n], int n) {
    cilk_for (int i = 0; i < n; ++i) {
        for (int j = 0; j < n; ++j) {
            double sum = 0.0;
            for (int k = 0; k < n; ++k) {
                sum += A[i][k] * B[k][j];
            }
            C[i][j] = sum;
        }
    }
}
Here, each iteration of the outer cilk_for computes an entire row of CC independently, with the runtime scheduler balancing the work via theft of subranges if iterations vary in cost due to cache effects or data access patterns. This approach, refined across Cilk variants from MIT Cilk to OpenCilk, demonstrates scalable performance on multicore systems without manual partitioning.[4]

Advanced Features

Reducers and Hyperobjects

Reducers in Cilk are specialized hyperobjects designed to facilitate parallel reductions by combining results from multiple tasks using associative operations, such as summation or minimization, thereby avoiding data races on shared variables without the need for locks.[41] These operations are based on monoids, which consist of a data type, an identity element, and a binary reduce function that is associative and commutative.[42] For instance, a reducer for integer summation can be declared as cilk::reducer<cilk::monoid_plus<int>> sum(0);, where the identity is 0 and the operation is addition.[43] Hyperobjects represent a broader class of abstract data types in Cilk that enable thread-safe access to shared structures by providing each worker thread with a private, local view of the object, coordinated by the runtime to merge updates deterministically.[41] Examples include hypermaps, which support concurrent insertions and lookups similar to a hash map, ensuring that parallel tasks can modify the structure without races.[41] This view-based approach serializes accesses efficiently during strand joins, where child views are reduced into parent views using the defined monoid operation.[43] A representative example is implementing a parallel reduction, such as computing the sum of partial results across parallel loop iterations:
cilk::reducer<cilk::monoid_plus<int>> sum_reducer(0);
cilk_for(int i = 0; i < n; ++i) {
    int partial = compute(i);
    *sum_reducer += partial;
}
int final_sum = sum_reducer.get_value();
Here, each iteration's update is applied to the local view, and views are merged at synchronization points to produce the correct total.[43] This mechanism complements parallel loops by allowing safe accumulation of results from divided iterations.[42] The implementation relies on hypermaps to manage views, where each worker's local view is stored and lazily merged only when necessary, minimizing overhead by initializing views to the identity element on first access.[41] Merging occurs at procedure calls and returns, ensuring deterministic outcomes even under work-stealing.[43] Reducers originated in Cilk++ with basic support for monoid-based operations defined via C++ classes inheriting from cilk::monoid_base.[41] Intel Cilk Plus expanded this by introducing macros like CILK_C_DECLARE_REDUCER for up to five callbacks, including view and hypermap management, to handle more complex reductions.[42] In OpenCilk, the feature was streamlined to require only identity and reduce functions, eliminating macros and heap allocation, with version 3.0 (released May 2025) adding support for C++ struct and class members as reducers through improved hyperobject integration.[44][11]

Array Notation

Array notation in Intel Cilk Plus provides a declarative syntax for expressing data-parallel operations on arrays and vectors, enabling concise vectorization without explicit loops.[13] This extension allows programmers to specify sections of arrays using a triplet notation, which the compiler translates into efficient SIMD instructions on supported hardware.[26] The syntax uses array sections in the form array[begin:length:stride], where begin indicates the starting index (default 0), length the number of elements, and stride the step size (default 1).[13] For the entire array, [:] serves as shorthand.[26] Slicing supports multidimensional arrays through nested triplets, such as A[0:3][0:4], defining a rank-2 section with shape [3,4].[13] Negative strides are permitted for reverse traversal, though lengths must be positive integers.[13] Supported operations include element-wise arithmetic (+, -, *, /, %), comparisons, logical operators, and unary operations, applied in parallel across matching sections.[13] Assignments propagate values element-wise, as in C[:] = A[:] * B[:] + 3.0;, where scalars broadcast to the array size.[26] Reductions compute aggregates like sums or minima over sections using built-in functions, for example, double sum = __sec_reduce_add(A[:]);.[13] Function calls map over sections, provided the function is elemental and side-effect-free.[13] The compiler integrates array notation by generating SIMD code, defaulting to SSE2 vectorization (e.g., processing four 32-bit floats per instruction on 128-bit registers), with options like /QxAVX for advanced instruction sets.[26] Alignment directives, such as __declspec(align(16)) or __assume_aligned(), ensure optimal performance by avoiding unaligned loads.[26] Operations on overlapping sections yield undefined behavior unless they fully overlap with stride 1.[13] For vector addition, traditional loops can be replaced with C[:] = A[:] + B[:];, which the compiler vectorizes automatically, improving performance on SIMD-enabled processors without altering the serial semantics.[26] This feature is specific to Intel Cilk Plus and the Intel C++ Compiler; OpenCilk, a successor implementation, does not support array notation, focusing instead on core tasking primitives.[34] Functions cannot return array sections directly, requiring pointer-based workarounds.[13]

Elemental Functions and Pragmas

Elemental functions in Intel Cilk Plus enable developers to define scalar functions that the compiler automatically converts into vectorized forms for SIMD execution on arrays or in parallel loops. These functions are marked with the __declspec(vector) attribute (on Windows) or __attribute__((vector)) (on Linux) in their declaration, allowing the compiler to generate a short-vector version that applies the operation element-wise across multiple data elements simultaneously. This approach promotes SIMD-safe computations by ensuring that the function operates independently on each element without interdependencies.[21][13] Elemental functions can be invoked in various parallel contexts, including standard for loops (where the compiler may auto-vectorize), explicitly vectorized loops with #pragma simd, array notations for bulk operations, or _Cilk_for loops for concurrent task execution. Optional clauses such as uniform (for scalar arguments identical across lanes), linear (for arguments varying linearly with the lane index), and vectorlength (to specify the SIMD width) provide fine-grained control over vectorization behavior. This mechanism extends beyond simple array expressions by allowing custom scalar logic to be parallelized at the SIMD level.[13][45] The #pragma simd directive complements elemental functions by mandating vectorization of for loops, particularly those where the compiler's auto-vectorizer might hesitate due to perceived dependencies or complex control flow. Placed immediately before the loop, it instructs the compiler to process multiple iterations concurrently using SIMD instructions, dividing the loop into vector-sized chunks. Clauses like reduction support common operations such as summation, enabling safe accumulation across vector lanes; for instance, #pragma simd reduction(+:sum) allows a loop to compute a running total without races. This pragma follows similar restrictions to _Cilk_for loops, requiring integer or pointer-based iteration variables.[27][13] A representative example is computing a parallel dot product, where an elemental function for squaring elements can be combined with #pragma simd for vectorized accumulation. Consider defining an elemental square function:
__declspec(vector) float square(float x) {
    return x * x;
}
Then, in the dot product loop:
float dot_product(float* a, float* b, int n) {
    float sum = 0.0f;
    #pragma simd reduction(+:sum)
    for (int i = 0; i < n; i++) {
        sum += square(a[i]) * b[i];  // Vectorized element-wise
    }
    return sum;
}
Here, square applies SIMD operations to chunks of a and b, while the reduction clause handles the summation across lanes and iterations. This yields efficient vectorization on modern hardware, such as AVX instructions, for data-parallel workloads.[27] These features were core to Intel Cilk Plus from 2009 to 2017, integrating SIMD optimizations with task parallelism. These features are specific to Intel Cilk Plus and are not supported in OpenCilk, which emphasizes task parallelism over SIMD vectorization.[36]

Runtime and Implementation

Work-Stealing Scheduler

The work-stealing scheduler is a core component of Cilk's runtime system, designed to efficiently load-balance parallel computations across multiple processors. In this algorithm, each worker thread maintains a double-ended queue (deque) to store its local ready tasks, which are pushed onto the top of the deque during depth-first execution to prioritize local work. When a worker becomes idle, it randomly selects a victim worker and attempts to steal a task from the bottom of the victim's deque, promoting breadth-first scheduling for stolen tasks to balance load across the system. This randomized approach ensures provable performance bounds for fully strict multithreaded computations, where the expected running time $ T_P $ on $ P $ processors is $ O\left( \frac{T_1}{P} + T_\infty \log P \right) $, with $ T_1 $ denoting the total work and $ T_\infty $ the computational depth (span); the logarithmic factor arises from the expected number of steals needed to resolve dependencies, while constant factors remain low in practice. The Cilk runtime library implements this scheduler by managing a pool of worker threads, each with its own task frame stack and deque, where spawn/sync operations create stealable task frames that can be suspended and resumed across threads. Victim selection is performed via uniform random choice among other workers, minimizing contention through non-blocking operations on deques, such as the THE protocol for concurrent access. The core algorithm originated in MIT's Cilk implementations and has been refined in OpenCilk, which integrates it with the LLVM compiler infrastructure for modern multicore systems as of OpenCilk 3.0 released in May 2025, preserving the same efficiency guarantees while supporting extensions like non-blocking steals and improved compiler optimizations.[6][11] For example, in a recursive task like parallel mergesort on an unbalanced array partition, one worker may initially process a large subtree deeply, filling its deque minimally, while others idle; steals from the bottom of the deep worker's deque then distribute subtasks breadth-first, resolving the imbalance and achieving near-linear speedup.

Serial Elision and Compatibility

Serial elision in Cilk refers to the process by which the parallelism constructs—such as spawn, sync, and cilk_for—are effectively ignored or treated as no-ops when the program executes on a single thread, resulting in a serial C/C++ program with identical semantics to the original Cilk code.[19] The runtime system employs a nanoscheduler that detects single-threaded execution and skips the overhead of thread creation and synchronization, executing spawned procedures sequentially in a depth-first manner equivalent to their serial elision.[19] This mechanism ensures that Cilk programs compile and run without modification as standard C/C++ code on one core, preserving the original serial behavior and allowing developers to incrementally add parallelism to existing serial applications.[46] The compatibility of Cilk with serial C/C++ is a core design principle, enabling seamless integration without requiring changes to data types, memory management, or pointer semantics; for instance, a Cilk program's serial elision adheres to C's rules for pointers and dynamic allocation via malloc or new.[46] This backward compatibility supports gradual parallelization, where developers can parallelize hotspots in legacy code while maintaining overall serial executability.[47] In non-parallel environments, the runtime disables work-stealing and other multiprocessor features, further minimizing deviations from serial execution.[19] Performance in serial mode incurs zero theoretical overhead for elided parallelism, with the nanoscheduler ensuring that execution matches the serial elision's efficiency; empirical measurements show that operations like spawn and return add only about three times the cost of a standard C function call on x86 architectures, resulting in negligible slowdown for programs with sufficient thread granularity.[46] For example, benchmarks like Fibonacci demonstrate that Cilk on one processor runs only marginally slower than optimized serial C, typically within a small constant factor.[19] This low-overhead design facilitates testing and deployment in both serial and parallel contexts without performance penalties in single-threaded scenarios. This serial elision feature originated in the MIT Cilk project and has been preserved across variants, including Intel Cilk++ and the open-source OpenCilk (up to version 3.0 as of May 2025), where the serial projection of a program—replacing parallelism keywords with no-ops—executes identically on one core while maintaining high work efficiency, such as 97% in representative benchmarks.[47][11] OpenCilk explicitly ensures backward compatibility with earlier Cilk semantics for serial behavior.[47] To support debugging in parallel executions, Cilk provides tools like Cilkscreen, a dynamic race detector that instruments programs to identify data races during multithreaded runs without altering serial compatibility; it reports races only if they occur in the monitored execution, aiding verification of thread safety.[14] Successor tools in OpenCilk, such as Cilksan, extend this capability for determinacy race detection while preserving the serial elision model.[47]

References

User Avatar
No comments yet.