Reducing Interprocess Communication Overhead in Concurrent Programs
E N D
Presentation Transcript
Reducing Interprocess Communication Overhead in Concurrent Programs Erik Stenman Kostis Sagonas
Motivation • Concurrency as an abstraction is important. • Systems that need to interact with the outside world are hard to model without concurrency. • Unfortunately concurrency costs. • There are two types of runtime overheads: • ”Direct overhead” of concurrency primitives. • ”Indirect overhead” from hiding the data-flow from the optimizing compiler.
Goal of this work • Reduce the overhead of concurrency in concurrent programs. Idea • Optimize the code that implements process communication.We call this interprocess optimization, and we will present three techniques: • Rescheduling send. • Direct dispatch send • Interprocess inlining.
Rescheduling Send • Typically, when a process sends a message, it is because it wants the receiver to act on that message. • It is therefore in the interest of the sender to yield to the receiver and allow it to promptly act on the sent message. • We call this type of send, a rescheduling send.
Rescheduling Send Implementation: • The send operation suspends the currently active (sending) process. Benefits: • lower message passing latency. • better cache behavior (the receiver has access to the message while it is still hot in the cache).
Direct Dispatch Send • The sender contributes the remaining part of its time-slice to the receiver (hoping this would lead to a faster response). • The receiver then is woken up directly (ignoring the ready queue). • Overhead of the scheduler is eliminated. • If the receiver also performs a direct dispatch send back to the sender, interprocess communication becomes as fast as a function call.
Interprocess Inlining • Merge code from the sender with code from the receiver.
A message Processa The sender Process b The receiver
Known communication protocol.Can be optimized.Process b only needs to be ready to receive the communication.
The state of process b has changed. Without really participating in the actual communication.
Interprocess Inlining • Merge code from the sender with code from the receiver. • In the merged code, the overhead of the communication protocol can be optimized away. • We suggest using profiling to find situations where this is feasible. • This requires that the optimized code is guarded by a runtime test.
ref_server(V) -> receive {‘++’,From}-> From ! {self(),V+1}, ref_server(V+1); {‘:=’,From,NewV}-> From ! {self(),NewV}, ref_server(NewV); {‘=’,From}-> From ! {self(),V}, ref_server(V) end inc(Beta) -> Beta ! {‘++’,self()}, receive {Beta,Answer} -> Answer; end. An example in Erlang…
ref_server(V) -> receive {‘++’,From}-> From ! {self(),V+1}, ref_server(V+1); {‘:=’,From,NewV}-> From ! {self(),NewV}, ref_server(NewV); {‘=’,From}-> From ! {self(),V}, ref_server(V) end inc´(Beta) -> ifready(Beta)-> B_V=get(‘V’,Beta)+1, save(‘V’,B_V,Beta), B_V; true -> inc(Beta) end. …could be rewritten as
Ready Code merging: the general case g ’ - Head g - Head - Head Createmessage Createmessage Receive Copy Send g – tail Yes Restoreβ-State -Tail Send Extractedg – tail -Tail Saveβ-State Copy of-Tail
Code merging Can only be done if: • Code explosion does not occur. • The code does not suspend. • The control flow does not escape the included code. • The extracted code terminates (o/w, the sending process might hang after code merging.)
Code merging • These instructions are not extracted: • A call or a return. • Instructions that lead to the suspension of the process. • Throwing an exception. • Some BIFs are large and uncommon and not worth the effort to adapt for external execution. • Non-terminating code is unacceptable. • Hence, we do not accept any loops in the control flow graph.
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). f(Beta) -> Beta ! {hi,self()}, receive {Beta,fine} -> right; {Beta,_Other} -> wrong end. Createmessage send tail Code merging
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). f(Beta) -> Mess = {hi,self()}, ifready(Beta)-> NEWCODE; true -> Beta ! Mess, receive {Beta,fine} -> right; {Beta,_Answer} -> wrong end end. Test f-tail Code merging
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). NEWCODE: b-Mbox = [{hi,self()}], caseb-Mboxof [{hi, b-From}]-> b-From !b-{b-self(),fine}; _-> b-ignore b-end, save_state(Beta), COPY-OF-f-TAIL Code merging
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). NEWCODE: b-Mbox = [{hi,self()}], case [{hi,self()}]of [{hi, b-From}]-> b-From !b-{b-self(),fine}; _-> b-ignore b-end, save_state(Beta), COPY-OF-f-TAIL Code merging
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). NEWCODE: b-Mbox = [{hi,self()}], b-From = self(), b-From !b-{b-self(),fine}, save_state(Beta), COPY-OF-f-TAIL Code merging
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). NEWCODE: b-Mbox = [{hi,self()}], b-From = self(), self()!b-{b-self(),fine}, save_state(Beta), COPY-OF-f-TAIL Code merging
Return messages • An important special case • Handled by applying the same type of rewriting. • The ready test is replaced by a test that checks that both mailboxes are empty.
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). NEWCODE: b-Mbox = [{hi,self()}], b-From = self(), self()!b-{b-self(),fine}, save_state(Beta), COPY-OF-f-TAIL More Code merging
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). NEWCODE: b-Mbox = [{hi,self()}], b-From = self(), a-Mbox = {b-self(),fine}, save_state(Beta), COPY-OF-f-TAIL More Code merging
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). NEWCODE: a-Mbox = {b-self(),fine}, save_state(Beta), COPY-OF-f-TAIL More Code merging
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). NEWCODE: a-Mbox = {b-self(),fine}, save_state(Beta), case a-Mbox of {Beta,fine} -> right; {Beta,_Other} -> wrong end More Code merging
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). NEWCODE: a-Mbox = {b-self(),fine}, save_state(Beta), case {b-self(),fine} of {Beta,fine} -> right; {Beta,_Other} -> wrong end More Code merging
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). NEWCODE: a-Mbox = {b-self(),fine}, save_state(Beta), right More Code merging
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). NEWCODE: save_state(Beta), right More Code merging
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). More Code merging f´(Beta) -> ifready(Beta)->save_state(Beta), right; true -> f(Beta) end.
This optimization gives • Access to variables in another process with almost no overhead. (Two reads, one test, and two writes.) • The overhead of the communication protocol can be reduced. (Creating a tuple and switching on it.)
Profiling • We have profiled parts of Eddie and the AXD code and found: • That each send goes to one particular receive. • The receiving process is typically suspended with an empty mailbox. • The same profiling tool could be used in a dynamic compiler to find pairs of senders/receivers whose inter-process communication can be optimized.
Conclusions • Presented several different methods for cross-process optimization that reduce the overhead of interprocess communication. • No modifications to existing code are required. • Open issue is their integration into the Erlang development environment
That’s all folks! Praeterea censeo "0xCA" scribere Erlang posse.Happi
Problem • How does one find senders and receivers that communicate? • Static analysis • Problematic in a language with dynamic typing, dynamic process creation, communication through mailboxes, and asynchronous communication. • Profiling & dynamic recompilation
Interprocess inlining • Do: • Find two communicating processes. • Merge code from the sender with code from the receiver. • Optimize the merged code. • Get: • Reduced message passing. • Short-circuited code for switching on messages.
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). f(Beta) -> Beta ! {hi,self()}, receive {Beta,fine} -> right; {Beta,_Other} -> wrong end. An example in Erlang…
g() -> receive {hi,From}-> From ! {self(),fine}; _ -> ignore end, g(). f´(Beta) -> ifready(Beta)->save_state(Beta), right; true -> Beta ! {hi,self()}, receive {Beta,fine} -> right; {Beta,_Other} -> wrong end end. …could be rewritten as