在4.1.2及更早版本中实现GCC cas功能

时间:2018-07-30 16:21:36

标签: c linux gcc assembly x86

我的新公司项目,他们希望代码运行在32位,编译服务器是带有 GCC 4.1.1 CentOS 5.0
在项目中使用的功能很多,例如__sync_fetch_and_add GCC 4.1.2 及更高版本中给出。

有人告诉我无法升级GCC版本,因此在谷歌搜索了几个小时后,我不得不提出另一个解决方案。

当我编写演示进行测试时,我得到的答案是错误的,代码打击想替换功能__sync_fetch_and_add

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

static int count = 0;

int compare_and_swap(int* reg, int oldval, int newval) 
{
    register char result;
#ifdef __i386__
    __asm__ volatile ("lock; cmpxchgl %3, %0; setz %1" 
                     : "=m"(*reg), "=q" (result) 
                     : "m" (*reg), "r" (newval), "a" (oldval) 
                     : "memory");
    return result;
#elif defined(__x86_64__)
    __asm__ volatile ("lock; cmpxchgq %3, %0; setz %1" 
                     : "=m"(*reg), "=q" (result) 
                     : "m" (*reg), "r" (newval), "a" (oldval) 
                     : "memory");
    return result;
#else
    #error:architecture not supported and gcc too old
#endif

}

void *test_func(void *arg)
{
    int i = 0;
    for(i = 0; i < 2000; ++i) {
        compare_and_swap((int *)&count, count, count + 1);
    }

    return NULL;
}

int main(int argc, const char *argv[])
{
    pthread_t id[10];
    int i = 0;

    for(i = 0; i < 10; ++i){
        pthread_create(&id[i], NULL, test_func, NULL);
    }

    for(i = 0; i < 10; ++i) {
        pthread_join(id[i], NULL);
    }
    //10*2000=20000
    printf("%d\n", count);

    return 0;
}

如果我得到了错误的结果:

[root@centos-linux-7 workspace]# ./asm
17123
[root@centos-linux-7 workspace]# ./asm
14670
[root@centos-linux-7 workspace]# ./asm
14604
[root@centos-linux-7 workspace]# ./asm
13837
[root@centos-linux-7 workspace]# ./asm
14043
[root@centos-linux-7 workspace]# ./asm
16160
[root@centos-linux-7 workspace]# ./asm
15271
[root@centos-linux-7 workspace]# ./asm
15280
[root@centos-linux-7 workspace]# ./asm
15465
[root@centos-linux-7 workspace]# ./asm
16673

我在这行中意识到

compare_and_swap((int *)&count, count, count + 1); 

count + 1是错的!

然后如何实现与__sync_fetch_and_add相同的功能。当第三个参数恒定时,compare_and_swap函数将起作用。

顺便说一句,compare_and_swap函数对吗?我只是为此搜索Google,不熟悉汇编程序。

我对这个问题感到绝望。

……………………………………………………………………………………………………………… ……………………………………

看到下面的答案后,我用了一会儿就得到了正确的答案,但似乎更加困惑。 这是代码:

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

static unsigned long  count = 0;

int sync_add_and_fetch(int* reg, int oldval, int incre) 
{
    register char result;
#ifdef __i386__
    __asm__ volatile ("lock; cmpxchgl %3, %0; setz %1" : "=m"(*reg), "=q" (result) : "m" (*reg), "r" (oldval + incre), "a" (oldval) : "memory");
    return result;
#elif defined(__x86_64__)
    __asm__ volatile ("lock; cmpxchgq %3, %0; setz %1" : "=m"(*reg), "=q" (result) : "m" (*reg), "r" (newval + incre), "a" (oldval) : "memory");
    return result;
#else
    #error:architecture not supported and gcc too old
#endif

}


void *test_func(void *arg)
{
    int i=0;
    int result = 0;
    for(i=0;i<2000;++i)
    {
        result = 0;
        while(0 == result)
        {
            result = sync_add_and_fetch((int *)&count, count, 1);
        }
    }

    return NULL;
}

int main(int argc, const char *argv[])
{
    pthread_t id[10];
    int i = 0;

    for(i=0;i<10;++i){
        pthread_create(&id[i],NULL,test_func,NULL);
    }

    for(i=0;i<10;++i){
        pthread_join(id[i],NULL);
    }
    //10*2000=20000
    printf("%u\n",count);

    return 0;
}

答案正好是20000,所以我认为当您使用sync_add_and_fetch函数时,应该使用while循环是愚蠢的,所以我这样写:

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

static unsigned long  count = 0;

int compare_and_swap(int* reg, int oldval, int incre) 
{
    register char result;
#ifdef __i386__
    __asm__ volatile ("lock; cmpxchgl %3, %0; setz %1" : "=m"(*reg), "=q" (result) : "m" (*reg), "r" (oldval + incre), "a" (oldval) : "memory");
    return result;
#elif defined(__x86_64__)
    __asm__ volatile ("lock; cmpxchgq %3, %0; setz %1" : "=m"(*reg), "=q" (result) : "m" (*reg), "r" (newval + incre), "a" (oldval) : "memory");
    return result;
#else
    #error:architecture not supported and gcc too old
#endif

}

void sync_add_and_fetch(int *reg,int oldval,int incre)
{
    int ret = 0;
    while(0 == ret)
    {
       ret = compare_and_swap(reg,oldval,incre);
    }
}

void *test_func(void *arg)
{
    int i=0;
    for(i=0;i<2000;++i)
    {
        sync_add_and_fetch((int *)&count, count, 1);
    }

    return NULL;
}

int main(int argc, const char *argv[])
{
    pthread_t id[10];
    int i = 0;

    for(i=0;i<10;++i){
        pthread_create(&id[i],NULL,test_func,NULL);
    }

    for(i=0;i<10;++i){
        pthread_join(id[i],NULL);
    }
    //10*2000=20000
    printf("%u\n",count);

    return 0;
}

但是当我在g ++ -g -o asm asm.cpp -lpthread之后用./asm运行此代码时,asm停留了5分钟以上,请参见另一个终端的顶部:

3861根19 0 102m 888732 S 400 0.0 2:51.06 asm

我只是感到困惑,这段代码不一样吗?

3 个答案:

答案 0 :(得分:1)

64位的compare_and_swap是错误的,因为它交换了64位,而int只有32位。

compare_and_swap应该在重试它直到成功的循环中使用。

答案 1 :(得分:1)

您的结果对我来说不错。 lock cmpxchg在大多数情况下都是成功的,但是如果另一个核心击败了您,它将失败。您正在尝试20万次尝试cmpxchg count+1,而不是20k原子增量。

要使用内联汇编编写__sync_fetch_and_add,则需要使用lock xadd。它是专门为实现fetch-add而设计的。

要实现其他操作(如fetch-or或fetch-and),则需要CAS重试循环,如果您实际上需要旧值。因此,您可以使用sync-and和内存目标,使该函数的版本不返回旧值,并且是 just 一个lock and,而无需提取。 (编译器内置的文件可以根据是否需要结果来进行优化,但是内联asm实现没有机会根据该信息选择asm。)

为了提高效率,请记住andoradd和许多其他指令可以使用立即数操作数,因此使用"re"(src)约束是合适的(而不是{{1 }}在x86-64上用于"ri",因为这会使立即数太大。https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html)。但是,cmpxchg,xadd和xchg当然不能使用立即数。

我建议您查看使用内置函数的现代gcc的编译器输出(例如,在http://godbolt.org/上),以查看编译器的功能。


但是要注意,内联汇编程序可以在一组周围的代码下正确编译,但不能在给定不同代码的情况下正确编译。例如如果周围的代码在使用CAS后复制了一个值(可能不太可能),则编译器可能会决定为int64_t"=m"(*reg)给asm模板两个不同的内存操作数,但是您的asm模板假定它们会总是相同的地址。

IDK(如果gcc4.1支持),但是 "m"(*reg)将声明读/写内存操作数。否则,也许您可​​以使用匹配约束来说输入与早期操作数(例如"+m"(*reg))位于同一位置。但这可能仅适用于寄存器,而不适用于内存,我没有检查。


"0"(*reg)是一个错误:cmpxchg writes EAX on failure

告诉编译器您未修改reg,然后编写可对其进行修改的asm模板是不正确的。踩到编译器的脚趾,您将得到无法预测的行为。

有关"a" (oldval)的安全内联汇编包装,请参见c inline assembly getting "operand size mismatch" when using cmpxchg 。它是为gcc6标志输出编写的,因此您必须将其以及可能的其他一些语法详细信息回传到硬壳的旧gcc4.1。

该答案还解决了返回旧值的问题,因此不必单独加载。

(对于我来说,使用古老的gcc4.1听起来是个坏主意,特别是对于编写多线程代码。从将带有lock cmpxchg内建函数的工作代码移植到手动滚动的asm上,错误空间很大。使用更新的编译器,例如稳定的gcc5.5(如果不是gcc7.4),则有所不同,但可能会更小。)

如果您打算使用__sync内置程序来重写代码,那么理智的事情就是使用C11 __sync或GNU C更现代的stdatomic.h内置程序来重写代码。替换__atomic

尽管如此,Linux内核确实成功地将内联asm用于手动滚动原子,所以这肯定是可能的。

答案 2 :(得分:1)

如果您确实处于这种困境中,那么我将从以下头文件开始:

#ifndef   SYNC_H
#define   SYNC_H
#if defined(__x86_64__) || defined(__i386__)

static inline int  sync_val_compare_and_swap_int(int *ptr, int oldval, int newval)
{
    __asm__ __volatile__( "lock cmpxchgl %[newval], %[ptr]"
                        : "+a" (oldval), [ptr] "+m" (*ptr)
                        : [newval] "r" (newval)
                        : "memory" );
    return oldval;
}

static inline int  sync_fetch_and_add_int(int *ptr, int val)
{
    __asm__ __volatile__( "lock xaddl %[val], %[ptr]"
                        : [val] "+r" (val), [ptr] "+m" (*ptr)
                        :
                        : "memory" );
    return val;
}


static inline int  sync_add_and_fetch_int(int *ptr, int val)
{
    const int  old = val;
    __asm__ __volatile__( "lock xaddl %[val], %[ptr]"
                        : [val] "+r" (val), [ptr] "+m" (*ptr)
                        :
                        : "memory" );
    return old + val;
}

static inline int  sync_fetch_and_sub_int(int *ptr, int val) { return sync_fetch_and_add_int(ptr, -val); }
static inline int  sync_sub_and_fetch_int(int *ptr, int val) { return sync_add_and_fetch_int(ptr, -val); }

/* Memory barrier */
static inline void  sync_synchronize(void) { __asm__ __volatile__( "mfence" ::: "memory"); }

#else
#error Unsupported architecture.
#endif
#endif /* SYNC_H */

相同的扩展内联程序集可同时用于x86和x86-64。仅实现int类型,您确实需要用__sync_synchronize()替换可能的sync_synchronize()调用,并用__sync_...()替换每个sync_..._int()调用。

要进行测试,您可以使用例如

#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include "sync.h"

#define  THREADS   16
#define  PERTHREAD 8000

void *test_func1(void *sumptr)
{
    int *const sum = sumptr;
    int        n = PERTHREAD;
    while (n-->0)
        sync_add_and_fetch_int(sum, n + 1);
    return NULL;
}

void *test_func2(void *sumptr)
{
    int *const sum = sumptr;
    int        n = PERTHREAD;
    while (n-->0)
        sync_fetch_and_add_int(sum, n + 1);
    return NULL;
}

void *test_func3(void *sumptr)
{
    int *const sum = sumptr;
    int        n = PERTHREAD;
    int        oldval, curval, newval;
    while (n-->0) {
        curval = *sum;
        do {
            oldval = curval;
            newval = curval + n + 1;
        } while ((curval = sync_val_compare_and_swap_int(sum, oldval, newval)) != oldval);
    }
    return NULL;
}

static void *(*worker[3])(void *) = { test_func1, test_func2, test_func3 };

int main(void)
{
    pthread_t       thread[THREADS];
    pthread_attr_t  attrs;
    int             sum = 0;
    int             t, result;

    pthread_attr_init(&attrs);
    pthread_attr_setstacksize(&attrs, 65536);
    for (t = 0; t < THREADS; t++) {
        result = pthread_create(thread + t, &attrs, worker[t % 3], &sum);
        if (result) {
            fprintf(stderr, "Failed to create thread %d of %d: %s.\n", t+1, THREADS, strerror(errno));
            exit(EXIT_FAILURE);
        }
    }
    pthread_attr_destroy(&attrs);

    for (t = 0; t < THREADS; t++)
        pthread_join(thread[t], NULL);

    t = THREADS * PERTHREAD * (PERTHREAD + 1) / 2;
    if (sum == t)
        printf("sum = %d (as expected)\n", sum);
    else
        printf("sum = %d (expected %d)\n", sum, t);

    return EXIT_SUCCESS;
}

不幸的是,我没有要测试的较旧版本的GCC,因此仅在x86和x86-64上使用GCC 5.4.0和GCC-4.9.3(使用-O2)进行了测试。 Linux。

如果您发现上述任何错误或问题,请在评论中告知我,以便我根据需要进行验证和修复。