Sol Boucher presented his thesis proposal today via Zoom. It was well attended and I think many of us are already looking forward to his defense.
Lightweight Preemptable Functions (LPF)
Function calls require some knowledge of the cost for a program to know whether it can invoke it or not in other time critical execution.
In the current space of multitasking, three approaches will be first reviewed: futures, threads, and processes. Futures rely on the runtime to provide asynchronous execution, but preemption requires yield points, and not all execution can support these points, nor can it be clear what this future will do. With threads, execution is fairly independent, but minimal information is provided toward scheduling and cancellation is hard. Processes provide good support for cancellation, except that on fork(), only one thread is required to be carried over in the new process. The other threads are canceled, which can result in inconsistent state.
The thesis proposal is: Introducing a novel abstraction for preemption at the granularity of a synchronous function call, which includes a timeout.
launch(function, timeout, argument) returning a continuation. If the timeout elapses, the continuation is returned, but it is then the choice of the caller for whether to resume it or cancel. This LPF executes within the same thread context as the caller, thereby reducing overhead. However, to resume the LPF, it will need a new stack. To support the timeout, it relies on a timer signal that can occur every ~5 microseconds. Launch / resume have overhead comparable to this, significantly better than fork or pthread_create. However, cancel is extremely expensive.
LPFs also have an issue with calling functions that are non-reentrant, similar to the rules governing signal handlers. To address this, the runtime provides selective relinking to capture what the LPF is calling via the global offset table (GOT). Some GOT entries point to dynamic libraries, other entries are initially pointing to the dynamic linker. This runtime support also needs to intercept thread local variables. This interception support imposes about 10ns of overhead, which is little above the cost of function calls themselves.
Microservice approaches have significant latency, often tends to hundreds of microseconds. Primarily the requirement to create a sufficient container, often via processes or virtualization. If the microservice was instead written safely and using LPFs, then the latency could be reduced toward the hardware bound as measured by communicating between VMs or committing transactions.
Cancellation cleanup is difficult in languages, such as C, that require explicit cleanup. In other languages, adding a new exception path for timeout and cancellation could then invoke the necessary destructors. Nonetheless, this can be expensive (perhaps single milliseconds).
Other possible future work:
Another cancellation step is the cost of unloading the mirrored library, so could the runtime instead track the changes made and then determine whether to roll back or discard.
Is it possible to reduce the overhead of the preemption signals or improving their granularity.
A discussion of how to do Computer Science well, particularly writing code and architecting program solutions.
Showing posts with label concurrency. Show all posts
Showing posts with label concurrency. Show all posts
Wednesday, April 22, 2020
Thursday, September 12, 2019
Thesis Proposal: Theoretical Foundations for Modern Multiprocessor Hardware
Naama Ben-David gave her proposal this morning on Theoretical Foundations for Modern Multiprocessor Hardware.
Is there a theoretical foundation for why exponential backoff is a good design? Exponential backoff is a practically developed algorithm that 0.
To develop such a foundation, we need to a model of time; however, requests are asynchronous and not according to a single time source. To address this, model time with adversarial scheduling. Thus when performing a request, there are three sources of delay:
Is there a theoretical foundation for why exponential backoff is a good design? Exponential backoff is a practically developed algorithm that 0.
To develop such a foundation, we need to a model of time; however, requests are asynchronous and not according to a single time source. To address this, model time with adversarial scheduling. Thus when performing a request, there are three sources of delay:
- self-delay: backoff, sleep, local computation
- system-delay: interrupts, context switches
- contention-delay: delay caused by contention
Given this model, the adversary can, to a limited degree, decide when requests that an entity's request have passed from self-delay into the system delay can then move to contention-delay and ultimately be completed.
In BBlelloch'17, this model was applied and the work measured for different approaches.
- With no backoff, there is omega(n3) work.
- Exp backoff reduces to theta(n2 log n) bound on work
- The paper also proposes a new algorithm that has high probability of O(n2)
The second phase of work is developing simple and efficient algorithms for systems that have non-volatile memory (NVRAM). With NVRAM, on a crash or system failure, the contents in memory persist across reboot (or other restore). This permits the system to restore the running program(s) to a finer degree than happens from auto-saves or other current techniques. However, systems also have caches, which are not persistent. Caches are presently managed by hardware and make decisions as to when to write contents back to memory. Algorithms must work with the caches to ensure that results are safely in memory at selected points of execution. There are a variety of approaches for how to select these points.
The third phase of work is modeling RDMA (remote direct memory access) systems. Can there be a model of the different parts of such a system: memory, NIC (network interface card), and CPU? Then explore the contention as well as possible failures in the system.
One scheme is for every processes to also be able to send messages on behalf of its shared memory neighbors, so that even if a process fails, its ability to participate in algorithms, such as consensus, is still possible.
Being a proposal, ongoing work will work on instantiations of these algorithms to measure the practical performance.
Saturday, March 23, 2019
Repost: Code Smells ... Is concurrency natural?
Writing parallel code is not considered easy, but it can be a natural approach to some problems for novices. When a beginner wants something to happen twice concurrently, the reasonable thing would be to do what works once, a second time. Instead, this may conflict with other constructs of the language, such as main() or having to create threads. See more here.
Thursday, March 7, 2019
Talk: Concurrent Data Structures for Non-Volatile Memory
Today, Michal Friedman, gave a talk on Concurrent Data Structures for Non-Volatile Memory.
Future systems will contain non-volatile memory. This is memory that exhibits normal DRAM characteristics, but can maintain its contents even across power failures. In current systems, caches update memory on either evictions and flushes. Flushes, however, impose overhead due to the memory access time and overriding the write-back nature of most caches.
Linearizability is one definition for concurrency governing the observation of the operations. This can be extended to durable linearizability being on a durable system, such that data is flushed before global visibility (initialization), flush prior operations (dependence), and persist operations before they complete (completion). But a further extension is required to know when a sequence of operations are complete, beyond just taking snapshots of the memory state.
Relaxed, durable, and log versions of lock-free queue that extend Michael and Scott's baseline queue implementation. Each version provides stronger guarantees: relaxed are the existing augmented with a sync operation to snapshot state, durable preserves the data structure across failures, log identifies the specific state. The main guarantee is that the data structure will be consistent for any set of thread crashes, which is stronger than the lock-free guarantee.
We do this by extending the prior lock-free versions that include memory flushes of key state, and that later update which see volatile state will flush that state before completing their operations. This meets the durable linearizability. And can be extended by also have a log of operations that are updated and maintained before the operations themselves execute. These logs are per-thread, so as to be unordered and to be individually stateful.
The relaxed version implements sync by creating a special object that indicates a snapshot is occurring. If other concurrent operations find this object, they take over the snapshot and continue persisting the state before completing its own operation. Thus a snapshot does not block other operations, but still occurs at that point in the sequence of operations.
Based on performance measurements, the relaxed performs similar to the baseline implementation, while the durable and log-based implementations run slower than the relaxed but with similar performance.
Finally, TSO provides us a guarantee that the stores will reach the cache line in a desired order and not require flushing between writes.
Wednesday, October 17, 2018
Thesis Defense: Practical Concurrency Testing
Ben Blum defended his dissertation work today on Practical Concurrency Testing. What follows are the notes from that defense.
To prove that a program is correct across arbitrary concurrency. There are three testing approaches:
unit testing of the most likely, stress testing that is not systematic, and verification that requires separate tools and techniques to describe.
Landslide is a proposed technique that is based on Stateless Model Checking (Godefroid '97), which tests a different execution interleaving on every iteration. However, the naive interleaving provides O(2^n) states to test. [Flanagan '05] identified equivalent interleavings and [Musuvathi '08] proposed heuristic orderings to identify the possible bugs faster. This approach can often require annotations, so adoption requires automated instrumentation. This space is addressing further concurrency problems such as weak memory models, but hardware transactional memory is still open.
This instrumentation requires preemption points. Finer-grained finds more bugs, but increases the states to test. Bugs / failures follow certain cases, such as use-after-free, deadlocks, assertion failures, and invalid memory accesses. Dynamic data-race analysis can help inform the necessary preemption points.
As a reminder, a data race:
- one or more accesses is write
- threads are not holding the same mutex
- Nor is there other ordering requirements (condition variable, etc)
Quicksand applies this analysis to select different smaller problem spaces using subsets of possible preemption points. Each subset also represents smaller parts of the larger possible problem space. If these subsets are all satisfied, then represents a full verification of the program. Prior work explored using APIs such as mutex_lock/unlock, or using every shared variable access as preemption points.
This tester is deployed in OS courses at CMU, PSU, and U Chicago. Manual annotation is not viable for students, especially those struggling for whom the traces would be valuable. That said, students regularly deploy ad-hoc synchronization, such as while (!ready) yield();, requires heuristics as the naive model checking must test every possible count of yielding and its interleaving.
When used by students, about 75% of tested kernels / libraries have identifiable bugs from the testing framework. For the tested submissions (7 semesters) of students at CMU, there is an improvement in grades, but it is not statistically significant when correcting for the opt-in bias. Most students are then able to fix their bugs found by the tool.
Hardware transactional memory poses a separate challenge for model checking. Aborted transactions are observationally equivalent to an immediately failed transaction. Furthermore, all transactions must be assumed to abortable, as there are many possible causes of aborts. As prior posts covered, this fact requires that any transaction have a valid abort path. And this abort path requires most of the verification.
Testing Landslide using hand-written tests, transactional data structures, and a TSX-based spinlock. Each set of tests has a concurrency or performance bug in the implementations. What about demonstrating that there are no bugs in implementation? With 10 hours of CPU time, verification is only possible for small cases on complex code. That said, practical testing so far only requires <4 preemptions to create the buggy scenario. There can be other bugs requiring an increasingly complex ordering, but generally those are very rare.
Abstraction reduction [Simsa '13], works to reduce primitives within implementations to verified components, such as mutual exclusion, etc. Using this technique then allows Landslide to verify the complex HTM implementations at higher thread counts.
In attendance are the recent instructors of Operating Systems and the TAs.
To prove that a program is correct across arbitrary concurrency. There are three testing approaches:
unit testing of the most likely, stress testing that is not systematic, and verification that requires separate tools and techniques to describe.
Landslide is a proposed technique that is based on Stateless Model Checking (Godefroid '97), which tests a different execution interleaving on every iteration. However, the naive interleaving provides O(2^n) states to test. [Flanagan '05] identified equivalent interleavings and [Musuvathi '08] proposed heuristic orderings to identify the possible bugs faster. This approach can often require annotations, so adoption requires automated instrumentation. This space is addressing further concurrency problems such as weak memory models, but hardware transactional memory is still open.
This instrumentation requires preemption points. Finer-grained finds more bugs, but increases the states to test. Bugs / failures follow certain cases, such as use-after-free, deadlocks, assertion failures, and invalid memory accesses. Dynamic data-race analysis can help inform the necessary preemption points.
As a reminder, a data race:
- one or more accesses is write
- threads are not holding the same mutex
- Nor is there other ordering requirements (condition variable, etc)
Quicksand applies this analysis to select different smaller problem spaces using subsets of possible preemption points. Each subset also represents smaller parts of the larger possible problem space. If these subsets are all satisfied, then represents a full verification of the program. Prior work explored using APIs such as mutex_lock/unlock, or using every shared variable access as preemption points.
This tester is deployed in OS courses at CMU, PSU, and U Chicago. Manual annotation is not viable for students, especially those struggling for whom the traces would be valuable. That said, students regularly deploy ad-hoc synchronization, such as while (!ready) yield();, requires heuristics as the naive model checking must test every possible count of yielding and its interleaving.
When used by students, about 75% of tested kernels / libraries have identifiable bugs from the testing framework. For the tested submissions (7 semesters) of students at CMU, there is an improvement in grades, but it is not statistically significant when correcting for the opt-in bias. Most students are then able to fix their bugs found by the tool.
Hardware transactional memory poses a separate challenge for model checking. Aborted transactions are observationally equivalent to an immediately failed transaction. Furthermore, all transactions must be assumed to abortable, as there are many possible causes of aborts. As prior posts covered, this fact requires that any transaction have a valid abort path. And this abort path requires most of the verification.
Testing Landslide using hand-written tests, transactional data structures, and a TSX-based spinlock. Each set of tests has a concurrency or performance bug in the implementations. What about demonstrating that there are no bugs in implementation? With 10 hours of CPU time, verification is only possible for small cases on complex code. That said, practical testing so far only requires <4 preemptions to create the buggy scenario. There can be other bugs requiring an increasingly complex ordering, but generally those are very rare.
Abstraction reduction [Simsa '13], works to reduce primitives within implementations to verified components, such as mutual exclusion, etc. Using this technique then allows Landslide to verify the complex HTM implementations at higher thread counts.
In attendance are the recent instructors of Operating Systems and the TAs.
Tuesday, September 6, 2016
When is it signaled?
Signals are a mechanism for notifying a process of a simple event. Most programmers and programs can ignore them, as the default behaviors are reasonable and taking steps to handle them would greatly increase the program complexity. But when teaching future computer scientists, we want the programmers to know about these mechanisms and therefore properly understand the functioning of the system.
In working with signals, the developing programmers are often exposed to their first dose of concurrency. The idea that execution can be happening in simultaneous, arbitrary orders except when action is taken by the program. With signals, a program can do several things:
We are going to consider the problem of sending and receiving a signal. After a signal is sent, when does the other process receive it? Students make the assumption that when the sending function (i.e., kill) has returned, the signal has been sent *and received*. However, I have found no text that explicitly guarantees this condition. Instead, I prepared a simple program (source at the end of this post) to test this condition.
As signals are communicated through the operating system, we want a different mechanism for measuring simultaneity, in this case shared memory. The experiment program will create and set up a small space of shared memory between two processes. Next, it will wait until both programs are running in a known state (i.e., barrier). Then one (the parent) will signal the other (the child), while measuring how long it takes to send, as well as receive the signal. Finally, run this experiment a million times.
On my current Ubuntu box with Skylake processors, the experimental measurements show that 80% of the time, the child lasts for an average of 40 cycles after kill returns. The maximum time is almost 1200 cycles. Assuming that each core's clock has a smaller skew, this effectively means that the child can continue to run even after the function has returned.
Source code follows: (compiled with gcc version 4.8.3, with gcc -O3 -lrt)
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdint.h>
#include "rdtsc.h"
static const int ITER = 1000 * 1000;
int main(int argc, char** argv)
{
int shm_fd = shm_open("squig", O_CREAT | O_RDWR, (S_IREAD | S_IWRITE));
int count = 0, i;
uint64_t zero = 0, min = ~0x0, max = 0, sum = 0;
uint64_t minN = ~0x0, maxN = 0, sumN = 0;
write(shm_fd, &zero, 8); // Give the shared file "space"
void* msh = mmap(NULL, 4 * 1024, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
volatile uint64_t* tsc = msh;
for (i = 0; i < ITER; i++)
{
*tsc = 0;
pid_t chd = fork();
if (chd == 0)
{
// Give the compiler something to think about
while(zero < ~0x0) { *tsc = rdtsc(); zero++;}
}
else
{
// Wait for the child
while (*tsc == 0) {}
uint64_t st = *tsc; // When is it for the child?
kill(chd, SIGINT); // Send SIGINT to child
uint64_t k = rdtsc(); // Returned from kill
wait(NULL); // Reap
uint64_t delta = 0;
uint64_t en = *tsc; // When did the child end?
// K >, implies that kill returned after the child terminated
if (k > en)
{
count ++;
delta = k - en;
if (delta < minN) minN = delta;
if (delta > maxN) maxN = delta;
sumN += delta;
}
else
{
delta = en - k;
if (delta < min) min = delta;
if (delta > max) max = delta;
sum += delta;
}
}
}
printf("Min: %lx, Max: %lx, Avg: %lx\n", min, max, (sum / (ITER - count)));
printf("Min: %lx, Max: %lx, Avg: %lx\n", minN, maxN, (sumN / (count)));
printf("Percent Parent After: %lf\n", (count / (double)ITER));
return 0;
}
Update: Results also hold when sending SIGKILL.
In working with signals, the developing programmers are often exposed to their first dose of concurrency. The idea that execution can be happening in simultaneous, arbitrary orders except when action is taken by the program. With signals, a program can do several things:
- Provide and install a handler for one or more signals
- Block the receipt of one or more signals
- Send a signal to one or more processes
We are going to consider the problem of sending and receiving a signal. After a signal is sent, when does the other process receive it? Students make the assumption that when the sending function (i.e., kill) has returned, the signal has been sent *and received*. However, I have found no text that explicitly guarantees this condition. Instead, I prepared a simple program (source at the end of this post) to test this condition.
As signals are communicated through the operating system, we want a different mechanism for measuring simultaneity, in this case shared memory. The experiment program will create and set up a small space of shared memory between two processes. Next, it will wait until both programs are running in a known state (i.e., barrier). Then one (the parent) will signal the other (the child), while measuring how long it takes to send, as well as receive the signal. Finally, run this experiment a million times.
On my current Ubuntu box with Skylake processors, the experimental measurements show that 80% of the time, the child lasts for an average of 40 cycles after kill returns. The maximum time is almost 1200 cycles. Assuming that each core's clock has a smaller skew, this effectively means that the child can continue to run even after the function has returned.
Source code follows: (compiled with gcc version 4.8.3, with gcc -O3 -lrt)
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdint.h>
#include "rdtsc.h"
static const int ITER = 1000 * 1000;
int main(int argc, char** argv)
{
int shm_fd = shm_open("squig", O_CREAT | O_RDWR, (S_IREAD | S_IWRITE));
int count = 0, i;
uint64_t zero = 0, min = ~0x0, max = 0, sum = 0;
uint64_t minN = ~0x0, maxN = 0, sumN = 0;
write(shm_fd, &zero, 8); // Give the shared file "space"
void* msh = mmap(NULL, 4 * 1024, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
volatile uint64_t* tsc = msh;
for (i = 0; i < ITER; i++)
{
*tsc = 0;
pid_t chd = fork();
if (chd == 0)
{
// Give the compiler something to think about
while(zero < ~0x0) { *tsc = rdtsc(); zero++;}
}
else
{
// Wait for the child
while (*tsc == 0) {}
uint64_t st = *tsc; // When is it for the child?
kill(chd, SIGINT); // Send SIGINT to child
uint64_t k = rdtsc(); // Returned from kill
wait(NULL); // Reap
uint64_t delta = 0;
uint64_t en = *tsc; // When did the child end?
// K >, implies that kill returned after the child terminated
if (k > en)
{
count ++;
delta = k - en;
if (delta < minN) minN = delta;
if (delta > maxN) maxN = delta;
sumN += delta;
}
else
{
delta = en - k;
if (delta < min) min = delta;
if (delta > max) max = delta;
sum += delta;
}
}
}
printf("Min: %lx, Max: %lx, Avg: %lx\n", min, max, (sum / (ITER - count)));
printf("Min: %lx, Max: %lx, Avg: %lx\n", minN, maxN, (sumN / (count)));
printf("Percent Parent After: %lf\n", (count / (double)ITER));
return 0;
}
Update: Results also hold when sending SIGKILL.
Wednesday, June 17, 2015
Conference Attendance FCRC - Day 4 - PLDI
PLDI starts off this morning with Concurrency. As a student volunteer, I worked this session and was limited as to what I could note about the content itself.
Composing Concurrency Control - Introducing more diverse and finer-grained locking mechanisms. The tool works to develop a locking strategy that will guarantee serializability, abort-safety, opacity, and deadlock-freedom. It particularly works to integrate both locking schemes as well as transactional memory.
In the afternoon, I can dive into the semantics of the C language.
A Formal C Memory Model Supporting Integer-Pointer Casts - What optimizations are possible in the presence of pointers, pointer arithmetic, and integer-pointer casts? For example, can constants be propagated or is their location potentially targetable by a pointer? Other optimizations are explored in their paper. In practice, as code can generate arbitrary addresses, how can the compiler reason about any specific location in memory.
Defining the Undefinedness of C - Extending their prior work that gave semantics to defined behavior of C programs, which required doubling the rules to describe the semantic behavior. Fundamentally, any instance of undefined behavior that will be definitely encountered in an execution will invalidate that execution. For example, dividing by zero after a printf is valid to crash before the printf. The following code example is also undefined.
Composing Concurrency Control - Introducing more diverse and finer-grained locking mechanisms. The tool works to develop a locking strategy that will guarantee serializability, abort-safety, opacity, and deadlock-freedom. It particularly works to integrate both locking schemes as well as transactional memory.
In the afternoon, I can dive into the semantics of the C language.
A Formal C Memory Model Supporting Integer-Pointer Casts - What optimizations are possible in the presence of pointers, pointer arithmetic, and integer-pointer casts? For example, can constants be propagated or is their location potentially targetable by a pointer? Other optimizations are explored in their paper. In practice, as code can generate arbitrary addresses, how can the compiler reason about any specific location in memory.
Defining the Undefinedness of C - Extending their prior work that gave semantics to defined behavior of C programs, which required doubling the rules to describe the semantic behavior. Fundamentally, any instance of undefined behavior that will be definitely encountered in an execution will invalidate that execution. For example, dividing by zero after a printf is valid to crash before the printf. The following code example is also undefined.
return (x = 1) + (x = 2);Many of these cases are dependent on runtime behavior, and therefore a tool that can help identify them is valuable.
Tuesday, September 16, 2014
Atomic Weapons in Programming
In parallel programming, most of the time the use of locks is good enough for the application. And when it is not, then you may need to resort to atomic weapons. While I can and have happily written my own lock implementations, its like the story of a lawyer redoing his kitchen himself. It is not a good use of the lawyer's time unless he's enjoying it.
That said, I have had to use atomic weapons against a compiler. The compiler happily reordered several memory operations in an unsafe way. Using fence instructions, I was able to prevent this reordering, while not seeing fences in the resulting assembly. I still wonder if there was some information I was not providing.
Regardless, the weapons are useful! And I can thank the following presentation for illuminating me to the particular weapon that was needed, Atomic Weapons. I have reviewed earlier work by Herb Sutter and he continues to garner my respect (not that he is aware), but nonetheless I suggest any low-level programmer be aware of the tools that are available, as well as the gremlins that lurk in these depths and might necessitate appropriate weaponry.
That said, I have had to use atomic weapons against a compiler. The compiler happily reordered several memory operations in an unsafe way. Using fence instructions, I was able to prevent this reordering, while not seeing fences in the resulting assembly. I still wonder if there was some information I was not providing.
Regardless, the weapons are useful! And I can thank the following presentation for illuminating me to the particular weapon that was needed, Atomic Weapons. I have reviewed earlier work by Herb Sutter and he continues to garner my respect (not that he is aware), but nonetheless I suggest any low-level programmer be aware of the tools that are available, as well as the gremlins that lurk in these depths and might necessitate appropriate weaponry.
Thursday, February 24, 2011
VC++ Concurrency Runtime
I was not aware of any particular concurrency support in the VC++ environment, so I was delighted when a friend of mine posted about the VC++ Concurrency Runtime and lambda expressions. Therefore, while the article was about using lambda expressions, I learned about the concurrency support and especially learning of the parallel pattern library. Lambda expressions intrigue me, not for actually using them but rather I persist in imagining how cool they are. I shall save lambdas for another post.
Subscribe to:
Posts (Atom)