GCC 4.7的软件事务内存(STM)实现框架

本文以一个实例开始,介绍GCCSTM的实现框架。在后面的文章中也许可以进一步深入分析实现的更多细节。

博注:本文前面的部分分析来自对http://natsys-lab.blogspot.com/2012/05/software-transactional-memory-stm-in.html的翻译,后面的分析属于原创,特此说明。

GCC 4.7引入了新的惊人的功能 – 软件事务内存(STM)。虽然是仍处于试验阶段尚未优化的功能,但我们已经可以看看STM是如何工作的了。目前,最初GCC实现的是纯软件TM,即没有硬件支持。英特尔公司已经宣布在其Haswell的微体系结构中实现的事务性同步扩展(TSX)来支持硬件TM(HTM),现在我们也将有混合TM,即软件事务内存的硬件优化。

首先,要了解STM是什么,让我们考虑下面的简单程序:

    #include <iostream> 
    #include <thread>
 
    static const auto THR_NUM = 4;
    static const auto ITER_NUM = 1000 * 1000;

    static auto a = 0, b = 0,c = 0;

    static void
    thr_func()
    {
            for (auto i = 0; i < ITER_NUM; ++i) {
                   ++a;
                   b += 2;
                   c = a + b;
           }
    }

    int
    main(int argc, char *argv[])
    {
           std::thread thr[THR_NUM];

            for (auto &t : thr)
                   t = std::thread(thr_func);

            for (auto &t : thr)
                   t.join();

           std::cout << "a=" << a << " b=" << b
                   << " c=" << c << std::endl;

            return 0;
    }

现在尝试编译(不要忘了-std=c++11,因为C ++11还不是g++的默认选项)和运行程序。也许你会看到,a,b和c的值每次运行都在古怪地改变,例如:

 

       $./a.out 
       a=2139058 b=4316262 c=6455320

       $ ./a.out 
       a=2077152 b=4463948 c=6541100

 

结果是意料之中的,因为4个线程同时更新所有三个变量,所有变量都是以RMW(读 – 修改 – 写)的方式更新。现在,让我们将所有三个变量的操作放到一个事务中(是的,这非常像数据库事务),因此,所有的变量将被以原子的方式读取和写入:

        static void
        thr_func()
        {
               for (auto i = 0; i < ITER_NUM; ++i) 
                       __transaction_atomic {
                              ++a;
                              b += 2;
                              c = a + b;
                      }
        }

让我们使用-fgnu-tm编译代码,以使能GCC的STM,并重新运行程序。这个时候你会看到一致的数字,保持每次运行都相同:

        $ ./a.out 
        a=4000000 b=8000000 c=12000000

       $ ./a.out 
       a=4000000 b=8000000 c=12000000

这是很简单的例子,你可能会喜欢使用互斥量(mutex)。但是你可以参考Ulrich Drepper的“使用事务内存的并行编程”中列出的更复杂的例子,互斥量方法并不那么明显。很容易看到,STM将是相当有用的,例如,可以实现高度并行,自平衡的二进制搜索树可能需要锁定好些节点来在插入或删除时做旋转(传统上这样的数据结构的实现是通过为每个节点引入互斥量,但是这容易出现死锁)。

 

你可能会注意到,STM版本的程序运行速度要慢得多。因此,让我们分析一下它做这么久在干嘛。对于基本的调查,让我们strace一下程序的运行,并打印系统调用的统计:

        $ strace -f -c ./a.out

        ……..

        % time  seconds  usecs/call   calls  errors syscall
        —— ——— ———– ————– ———
         99.39 0.021295         11    1920     390 futex
        …….

因此,这意味着,在libitm的STM(GCC用 libitm库实现STM,你可以在ldd输出看到)通过futex系统调用实现,像普通的互斥量一样。在深入研究libitm内部之前,让我们更加谨慎地看看代码,并将事务代码分解成基本的读,写操作。我们有3个存储单元,变量a,b和c,我们对其执行读取和写入操作。第一个操作++a中,从内存中读取一个值,更新并回写,所以我们在这里有两个操作,一个读操作和一个写操作。接下来的b += 2是完全一样的:读值,加2,并将它写回。最后一个 c = a + b,是两个读(a和b)和一个写(对c)。此外,所有这些操作都在事务内部,所以我们要开始和提交事务。

 

要了解什么是怎么回事,thr_func()可以简化为如下:

 

       static void
        thr_func()
        {
               __transaction_atomic {
                      ++a;
              }
        }

并反汇编:

       

        push   %rbp
        mov    %rsp,%rbp
        mov   $0x29,%edi
        mov    $0x0,%eax
        callq  400fd8 <[email protected]>
        mov   $0x6052ec,%edi
        callq  4010b8 <[email protected]>
        add    $0x1,%eax
        mov    %eax,%esi
        mov   $0x6052ec,%edi
        callq  400fe8 <[email protected]>
        callq  400f48 <[email protected]>
        pop    %rbp
        retq

现在我们看到4个调用_ITM_*函数(info libitm的说明,GCC按照英特尔的Draft Specification ofTransactional Language Constructs for C++ (v1.1) 来实现事务,所以_ITM_前缀只是英特尔的命名惯例来做事务开始,事务提交和读(RU4)写(WU4)操作。

 

_ITM_beginTransaction()保存机器状态(对于x86,见 libitm/config/x86/sjlj.S),并调用GTM::gtm_thread::begin_transaction()(见libitm/beginend.cc)来对事务数据进行初始化,检查事务嵌套情况,并执行其他的准备步骤。

 

        .align 4
        .globl  SYM(_ITM_beginTransaction)
 
SYM(_ITM_beginTransaction):
        cfi_startproc
#ifdef __x86_64__
        leaq    8(%rsp), %rax
        subq    $56, %rsp
        cfi_def_cfa_offset(64)
        movq    %rax, (%rsp)
        movq    %rbx, 8(%rsp)
        movq    %rbp, 16(%rsp)
        movq    %r12, 24(%rsp)
        movq    %r13, 32(%rsp)
        movq    %r14, 40(%rsp)
        movq    %r15, 48(%rsp)
        movq    %rsp, %rsi
        call    SYM(GTM_begin_transaction)
        addq    $56, %rsp
        cfi_def_cfa_offset(8)
#else
        leal    4(%esp), %ecx
        movl    4(%esp), %eax
        subl    $28, %esp
        cfi_def_cfa_offset(32)
        movl    %ecx, 8(%esp)
        movl    %ebx, 12(%esp)
        movl    %esi, 16(%esp)
        movl    %edi, 20(%esp)
        movl    %ebp, 24(%esp)
        leal    8(%esp), %edx
#if definedHAVE_ATTRIBUTE_VISIBILITY || !defined __PIC__
        call    SYM(GTM_begin_transaction)
#elif defined __ELF__
        call    1f
1:      popl    %ebx
        addl    $_GLOBAL_OFFSET_TABLE_+[.-1b], %ebx
        call    SYM(GTM_begin_transaction)@PLT
        movl    12(%esp), %ebx
#else
# error "Unsupported PICsequence"
#endif
        addl    $28, %esp
        cfi_def_cfa_offset(4)
#endif
        ret
        cfi_endproc
 
        TYPE(_ITM_beginTransaction)
        SIZE(_ITM_beginTransaction)
其中的GTM_begin_transaction实际上不是一个直接的函数定义,而是定义在struct gtm_thread (见libitm/libitm_i.h)中的一个函数指针,这是对C++代码从汇编语言中调用,类似于函数别名。
struct gtm_thread
{

。。。

  // Invoked from assembly language, thus the "asm" specifier on
  // the name, avoiding complex name mangling.
#ifdef __USER_LABEL_PREFIX__
#define UPFX1(t) UPFX(t)
#define UPFX(t) #t
  static uint32_t begin_transaction(uint32_t, const gtm_jmpbuf *)
        __asm__(UPFX1(__USER_LABEL_PREFIX__) "GTM_begin_transaction") ITM_REGPARM;
#else
  static uint32_t begin_transaction(uint32_t, const gtm_jmpbuf *)
        __asm__("GTM_begin_transaction") ITM_REGPARM;
#endif

。。。

};

其中的ITM_REGPARM定义如下:

#ifdef __i386__
/* Only for 32-bit x86.  */
# define ITM_REGPARM __attribute__((regparm(2)))
#else
# define ITM_REGPARM
#endif

这是因为《IntelTransactional Memory Compiler and Runtime Application Binary Interface》文档中,选定使用__fastcall方式作为calling convention;而GCC和Intel 编译器在Linux上与__fastcall最接近的都是写为__attribute__(((regparm(2))),而不是__fastcall(Windows如此)。因此,为了能最大化在Linux和Windows之间共享代码,选择使用__attribute__(((regparm(2)))。

_ITM_commitTransaction()在libitm/beginend.cc中定义,并试图调用GTM::gtm_thread::trycommit()来提交事务,如果失败,就重新启动事务。GTM::gtm_thread::trycommit()中所有事务线程都在futex()中睡眠等待(在strace的输出中,我们可以看到)来写入修改后的数据。因此,这是事务最重的部分。

void ITM_REGPARM
_ITM_commitTransaction(void)
{
#if defined(USE_HTM_FASTPATH)
  // HTM fastpath.  If we are not executing a HW transaction, then we will be
  // a serial-mode transaction.  If we are, then there will be no other
  // concurrent serial-mode transaction.
  // See gtm_thread::begin_transaction.
  if (likely(htm_fastpath && !gtm_thread::serial_lock.is_write_locked()))
    {
      htm_commit();
      return;
    }
#endif
  gtm_thread *tx = gtm_thr();
  if (!tx->trycommit ())
    tx->restart (RESTART_VALIDATE_COMMIT);
}

最有趣的是在读取和写入操作。0x6052ec是变量a的地址。 _ITM_RU4和_ITM_WU4仅仅是一系列的跳转,在这个特定情况下,分别跳转至ml_wt_dispatch::load()和ml_wt_dispatch::store()。其中第一个函数接受变量的地址,第二个函数接受变量地址和存储的值。load()方法读取指定地址的内存区域,但在此之前,它会调用ml_wt_dispatch::pre_load()函数验证存储位置没有被锁定或更新,并重新启动事务(这些服务数据是使用对地址应用哈希函数得出的索引从全局表中得来)。store()调用ml_wt_dispatch::pre_write(),它锁定内存位置(该存储位置的所有服务数据,也采取相同的全局表),并在写入之前更新内存位置的版本(version)(在pre_load()中版本被检查为’最新’)。

在libitm/method-ml.cc中,class ml_wt_dispatch的实现代码中,有如下宏调用:
CREATE_DISPATCH_METHODS(virtual, )

CREATE_DISPATCH_METHODSlibitm/dispatch.h中定义如下:

// Creates ABI load/store methods for all types.
// See CREATE_DISPATCH_FUNCTIONS for comments.
#define CREATE_DISPATCH_METHODS(M, M2)  \
  CREATE_DISPATCH_METHODS_T (U1, M, M2) \
  CREATE_DISPATCH_METHODS_T (U2, M, M2) \
  CREATE_DISPATCH_METHODS_T (U4, M, M2) \
  CREATE_DISPATCH_METHODS_T (U8, M, M2) \
  CREATE_DISPATCH_METHODS_T (F, M, M2)  \
  CREATE_DISPATCH_METHODS_T (D, M, M2)  \
  CREATE_DISPATCH_METHODS_T (E, M, M2)  \
  CREATE_DISPATCH_METHODS_T (CF, M, M2) \
  CREATE_DISPATCH_METHODS_T (CD, M, M2) \
  CREATE_DISPATCH_METHODS_T (CE, M, M2)

 

进一步,CREATE_DISPATCH_METHODS_T的定义如下:

// Creates ABI load/store methods for all load/store modifiers for a particular
// type.
#define CREATE_DISPATCH_METHODS_T(T, M, M2) \
  ITM_READ_M(T, R, M, M2)                \
  ITM_READ_M(T, RaR, M, M2)              \
  ITM_READ_M(T, RaW, M, M2)              \
  ITM_READ_M(T, RfW, M, M2)              \
  ITM_WRITE_M(T, W, M, M2)               \
  ITM_WRITE_M(T, WaR, M, M2)             \
  ITM_WRITE_M(T, WaW, M, M2)

 

// Creates ABI load/store methods (can be made virtual or static using M,
// use M2 to create separate methods names for virtual and static)
// The _PV variants are for the pure-virtual methods in the base class.
#define ITM_READ_M(T, LSMOD, M, M2)                                       \
  M _ITM_TYPE_##T ITM_REGPARM ITM_##LSMOD##T##M2 (const _ITM_TYPE_##T *ptr) \
  {                                                                         \
    return load(ptr, abi_dispatch::LSMOD);                                  \
  }

 

#define ITM_WRITE_M(T, LSMOD, M, M2)                         \
  M void ITM_REGPARM ITM_##LSMOD##T##M2 (_ITM_TYPE_##T *ptr, \
                                           _ITM_TYPE_##T val)  \
  {                                                          \
    store(ptr, val, abi_dispatch::LSMOD);                    \
  }

 

这样,考虑读的情形,最终CREATE_DISPATCH_METHODS(virtual, )会扩展出如下函数:
virtual _ITM_TYPE_U4 ITM_REGPARM ITM_RU4(const _ITM_TYPE_U4 *ptr) \
{
    return load(ptr, abi_dispatch::R);                                  \
}
此外,还有如下宏:
/* The following typedefs exist to make the macro expansions below work
   properly.  They are not part of any API.  */
typedef uint8_t  _ITM_TYPE_U1;
typedef uint16_t _ITM_TYPE_U2;
typedef uint32_t _ITM_TYPE_U4;
typedef uint64_t _ITM_TYPE_U8;

 

#ifdef __i386__
/* Only for 32-bit x86.  */
# define ITM_REGPARM __attribute__((regparm(2)))
#else
# define ITM_REGPARM
#endif

有了这些前提,再考虑扩展_ITM_TYPE_U4和ITM_REGPARM,我们得到一个函数ITM_RU4:

virtual  uint32_t __attribute__((regparm(2))) ITM_RU4(const uint32_t *ptr) 
{
    return load(ptr, abi_dispatch::R);                                  
}

libitm/barrier.cc中还有如下宏调用:

CREATE_DISPATCH_FUNCTIONS(GTM::abi_disp()->, )

CREATE_DISPATCH_FUNCTIONSlibitm/dispatch.h中是一个很大的宏定义,其开始部分如下:

#define CREATE_DISPATCH_FUNCTIONS(TARGET, M2)  \
  CREATE_DISPATCH_FUNCTIONS_T (U1, TARGET, M2) \
  CREATE_DISPATCH_FUNCTIONS_T (U2, TARGET, M2) \
  CREATE_DISPATCH_FUNCTIONS_T (U4, TARGET, M2) \
  CREATE_DISPATCH_FUNCTIONS_T (U8, TARGET, M2) \
  CREATE_DISPATCH_FUNCTIONS_T (F, TARGET, M2)  \
  CREATE_DISPATCH_FUNCTIONS_T (D, TARGET, M2)  \
  CREATE_DISPATCH_FUNCTIONS_T (E, TARGET, M2)  \
  CREATE_DISPATCH_FUNCTIONS_T (CF, TARGET, M2) \
  CREATE_DISPATCH_FUNCTIONS_T (CD, TARGET, M2) \
  CREATE_DISPATCH_FUNCTIONS_T (CE, TARGET, M2) \

。。。

我们考虑CREATE_DISPATCH_FUNCTIONS_T (U4, TARGET, M2)扩展的情形:

 

CREATE_DISPATCH_FUNCTIONS_T (U4, TARGET, M2)

 

// Creates ABI load/store functions for all load/store modifiers for a
// particular type.
#define CREATE_DISPATCH_FUNCTIONS_T(T, TARGET, M2) \
  ITM_READ(T, R, TARGET, M2)                \
  ITM_READ(T, RaR, TARGET, M2)              \
  ITM_READ(T, RaW, TARGET, M2)              \
  ITM_READ(T, RfW, TARGET, M2)              \
  ITM_WRITE(T, W, TARGET, M2)               \
  ITM_WRITE(T, WaR, TARGET, M2)             \
  ITM_WRITE(T, WaW, TARGET, M2)

同样,ITM_READITM_WRITE宏定义如下:

// Creates ABI load/store functions that can target either a class or an
// object.
#define ITM_READ(T, LSMOD, TARGET, M2)                                 \
  _ITM_TYPE_##T ITM_REGPARM _ITM_##LSMOD##T (const _ITM_TYPE_##T *ptr) \
  {                                                                    \
    return TARGET ITM_##LSMOD##T##M2(ptr);                            \
  }
#define ITM_WRITE(T, LSMOD, TARGET, M2)                                    \
  void ITM_REGPARM _ITM_##LSMOD##T (_ITM_TYPE_##T *ptr, _ITM_TYPE_##T val) \
  {                                                                        \
    TARGET ITM_##LSMOD##T##M2(ptr, val);                                  \
  }

 

因此,我们可以展开得到如下函数:

_ITM_TYPE_U4 ITM_REGPARM_ITM_RU4  (const_ITM_TYPE_U4 *ptr)

{

    return GTM::abi_disp()->ITM_RU4(ptr);                            

}

这实际上就是:

uint32_t __attribute__((regparm(2)))  _ITM_RU4  (constuint32_t *ptr)

{

    return GTM::abi_disp()->ITM_RU4(ptr);                            

}

GTM::abi_disp()->ITM_RU4正好就是我们前面展开的ITM_RU4:

virtual  uint32_t __attribute__((regparm(2))) ITM_RU4(const uint32_t *ptr) 
{
    return load(ptr, abi_dispatch::R);                                  
}

因此,我们可以知道,_ITM_RU4实际调用的是libitm/method-ml.cc或者libitm/method-gl.cc或者libitm/method-serial.cc中实现的具体方法。

libitm支持几种方法用以事务的彼此同步。这些TM方法(或TM算法)来是用`abi_dispatch’的子类实现的,该类提供了一些方法用以事务性加载(loads)和存储(stores),以及用于回滚(rollback)和提交(commit)事务的回调(callbacks)。相互兼容的所有方法(即,甚至在使用不同的方法时,也能让并发运行的事务仍然正确同步的方法)都属于相同的TM方法组。可以使用在libitm_i.h中以“dispatch_”前缀开头的工厂方法来获得TM方法的指针。

例如,有如下:

extern abi_dispatch *dispatch_serial();
extern abi_dispatch *dispatch_serialirr();
extern abi_dispatch *dispatch_serialirr_onwrite();
extern abi_dispatch *dispatch_gl_wt();
extern abi_dispatch *dispatch_ml_wt();
extern abi_dispatch *dispatch_htm();

有两个特殊的方法,dispatch_serial和dispatch_serialirr,能够与所有方法兼容,因为它们是运行以完全串行模式下的运行事务的。

libitm对于刚启动的事务(但不一定是重新启动的事务)使用的默认方法,可以通过设置环境变量ITM_DEFAULT_METHOD来设置。该环境变量的值应该等于返回abi_dispatch子类的工厂方法的名称,但不使用“dispatch_”前缀(例如,“serialirr”,而不是GTM:: dispatch_serialirr())。请注意,这个环境变量只是libitm的一个提示,在未来可能不支持。

TM方法的状态在创建后就不改变,但他们确实会改变使用该方法的事务的状态。然而,由于每个事务的数据被几种方法使用,gtm_thread负责设置一个对于所有方法都有用的初始状态。在那之后,各方法负责对每个回滚(rollback)或提交(commit)(最外层的事务)复位/清除(resetting/clearing)此状态,从而接下来执行的事务不会受到先前事务的影响。还有与各方法组相关联的全局状态,在方法组之间切换时会被初始化或关闭(method_group::init() 和 fini())(参见retry.cc)。

点赞