Monday, March 28, 2016

The transaction_wrap feature in GCC

When using transactional memory, a common challenge is that functions in standard libraries cannot be called from transactions.  Sometimes, the incompatibility is unavoidable (for example, because the library does something that cannot be rolled back).  But in other cases, the implementation is not transaction safe, when other (less performant) implementations could be.

The "transaction_wrap" attribute allows you to specify that function w() should be called in place of o() whenever o() is called from within a transaction.  The syntax is not too cumbersome, but there are a few gotchas.  Here's an example to show how it all works:

// This is a quick demonstration of how to use transaction_wrap in GCC

#include <iostream>
using namespace std;

// We have a function called orig(), which we assume is implemented in a
// manner that is not transaction-safe.
int orig(int);

// This line says that there is a function called wrapper(), which is
// transaction-safe, which has the same signature as orig().  When a
// transaction calls orig(), we would like wrapper() to be called instead.
int wrapper(int) __attribute__((transaction_safe, transaction_wrap (orig)));

// Here is our original function.  It does two things:
//
// 1 - saves its operand to a volatile variable iii.  This is not
//     transaction-safe!
// 2 - adds one to its operand and returns the sum
volatile int iii;
int orig(int x) {
    iii = x;
    return x + 1;
}

// Here is our wrapper function.  It adds two to its operand and returns the
// sum.  Note that we have explicitly implemented this in a manner that
// differs from orig(), so that we can easily see which is called
int wrapper(int x) {
    return x + 2;
}

// Our driver function calls orig(1) from three contexts: nontransactional,
// atomic transaction, and relaxed transaction, and prints the result of each
// call
//
// Be warned: the behavior is not what you expect, because it depends on the
// TM algorithm that is used.  For serial-irrevocable (serialirr), the result
// is (2,2,2).  For serial, ml_wt, and gl_wt, the result is (2, 3, 3).  For
// htm, the result is (2,3,2).
int main() {
    int x = orig(1);
    cout << "orig(1) (raw) == " << x << endl;
    __transaction_atomic { x = orig(1); }
    cout << "orig(1) (atomic) == " << x << endl;
    __transaction_relaxed { x = orig(1); }
    cout << "orig(1) (relaxed) == " << x << endl;
    return 0;
}

Compile the code like this:

g++ -fgnu-tm -std=c++11 -O3 test.cc -o test

And as suggested in the comments, the output will depend on the ITM_DEFAULT_METHOD you choose:

ITM_DEFAULT_METHOD=serialirr ./test
orig(1) (raw) == 2
orig(1) (atomic) == 2
orig(1) (relaxed) == 2
ITM_DEFAULT_METHOD=serial ./test
orig(1) (raw) == 2
orig(1) (atomic) == 3
orig(1) (relaxed) == 3
ITM_DEFAULT_METHOD=ml_wt ./test
orig(1) (raw) == 2
orig(1) (atomic) == 3
orig(1) (relaxed) == 3
ITM_DEFAULT_METHOD=gl_wt ./test
orig(1) (raw) == 2
orig(1) (atomic) == 3
orig(1) (relaxed) == 3
ITM_DEFAULT_METHOD=htm ./test
orig(1) (raw) == 2
orig(1) (atomic) == 3
orig(1) (relaxed) == 2

No comments:

Post a Comment