【问题标题】:achieve GCC cas function for version 4.1.2 and earlier4.1.2及更早版本实现GCC cas功能
【发布时间】:2018-07-30 16:21:36
【问题描述】:

我的新公司项目,他们希望代码在 32 位上运行,编译服务器是带有 GCC 4.1.1CentOS 5.0,那真是一场噩梦.
项目中使用了很多函数,例如 GCC 4.1.2 及更高版本中提供的 __sync_fetch_and_add

我被告知无法升级GCC版本,所以我在谷歌搜索了几个小时后必须做出另一个解决方案。

我写demo测试的时候,刚刚得到了错误的答案,代码吹要替换函数__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 函数是这样吗?我只是在谷歌上搜索了一下,不熟悉组装。

我对这个问题感到绝望。

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

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

#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 888 732 S 400 0.0 2:51.06 asm

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

【问题讨论】:

  • 匹配约束是否适用于内存操作数?您需要确保*reg 对输入和输出操作数使用相同的内存位置,否则 gcc 会认为它可以使用它来复制值。还是gcc4.1支持"+m"(*reg)读写内存操作数?
  • 是否正在从源代码编译更新的 gcc 版本?\
  • @phuclv 编译一个较新的 gcc 版本不能在 gcc 4.1.1 的 centos5.0 上运行。找不到服务器功能是错误的。
  • @PeterCordes 寻找gcc页面,没有找到gcc4.1是否支持"+m"(*reg),在上面的代码中,demo用"=m"( *reg)。
  • 您是否生成了所需的 __sync 内置函数列表,包括每个函数使用的类型?

标签: c linux gcc assembly x86


【解决方案1】:

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

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

【讨论】:

  • 使用while循环后更新问题,还是有问题,有时间请帮我看看,谢谢。
【解决方案2】:

您的结果对我来说很合适。 lock cmpxchg 大部分时间都会成功,但如果另一个核心击败你,它就会失败。您正在尝试 cmpxchg count+1 20k,而不是 20k 原子增量。

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

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

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

我建议查看现代 gcc 的编译器 输出(例如在 http://godbolt.org/ 上),了解使用内置函数的函数,看看编译器做了什么。


但请注意,内联 asm 可以在给定一组周围代码的情况下正确编译,但在给定不同代码的情况下不能按照您期望的方式编译。例如如果周围的代码在使用 CAS 后复制了一个值(可能不太可能),编译器可能会决定为 asm 模板提供两个不同的内存操作数 "=m"(*reg)"m"(*reg),但您的 asm 模板假定它们将始终相同地址。

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


"a" (oldval) 是一个错误:cmpxchg writes EAX on failure

告诉编译器你保留一个未修改的 reg,然后编写一个修改它的 asm 模板是不行的。踩到编译器的脚趾,你会得到不可预知的行为。

请参阅 c inline assembly getting "operand size mismatch" when using cmpxchg 以获取 lock cmpxchg 的安全 inline-asm 包装器。它是为 gcc6 标志输出而编写的,因此您必须将其以及一些其他语法细节向后移植到硬壳的旧 gcc4.1。

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

(对我来说,使用古老的 gcc4.1 听起来是个坏主意,尤其是在编写多线程代码时。将带有 __sync 内置函数的工作代码移植到手动 asm 的错误空间很大。使用较新的编译器,例如稳定的 gcc5.5,如果不是 gcc7.4,则不同但可能更小。)

如果您要使用 __sync 内置函数重写代码,明智的做法是使用 C11 stdatomic.h 或 GNU C 更现代的 __atomic 内置函数重写它,旨在替换 __sync

不过,Linux 内核确实成功地将内联 asm 用于手动原子操作,因此这当然是可能的。

【讨论】:

  • 使用while循环后更新问题,还是有问题,有时间请帮我看看,谢谢。
  • @52coder:sync_fetch_and_add 不需要 while 循环,使用 lock xadd 代替 lock cmpxchg。你的函数命名现在没有意义了;您有一个名为 compare_and_swap 的函数,它只能添加,因此它不再是通用的比较和交换函数。
【解决方案3】:

如果你真的陷入这样的困境,我会从以下头文件开始:

#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 来测试,因此仅在 Linux 上使用 GCC 5.4.0 和 GCC-4.9.3 for x86 和 x86-64(使用 -O2)进行了测试。

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

【讨论】:

  • 感谢您的帮助,当我运行您的代码时,出现了错误。stackoverflow.com/questions/10437990/…
  • [root@localhost ~]# gcc -g -o stackoverflow stackoverflow.c -lpthread sync.h:在函数'sync_val_compare_and_swap_int'中:sync.h:7:错误:找不到寄存器在类 'AREG' 中,同时重新加载 'asm' [root@localhost ~]#
  • @52coder:你可以试试__asm__ __volatile__ ( "lock cmpxchgl %[newval], %[ptr]" : "+a" (oldval) : "a" (oldval), [newval] "r" (newval), [ptr] "m" (*ptr) : "memory" )吗?
  • 为什么要使用"+r" (val)(一个读/写操作数),然后为val 使用单独的只读输入?为什么不是*ptr"+m" 输入,而不是只读输入?内存破坏器可以修改该内存,但它仍然很奇怪。有关 OP 的另一个问题,请参阅我的 sync_xchg asm。 :Achieve window function InterlockedExchange in Linux。但是,是的,这就是您使用 lock xadd 的方式。
  • @52coder: 似乎没有优化,gcc 并没有弄清楚"+a" (oldval)"a" (oldval) 可以是CSE,所以它可以对两个操作数使用EAX。 godbolt.org/g/gthsox。如您所见,它还破坏了sync_fetch_and_add_int,因为它将只读版本放在不同的寄存器中(使用 ECX 然后读取 EAX)。但是对于-O3,一切正常,因为编译器看到的是同一个变量。 最佳解决方案:删除操作数的冗余只读副本。 godbolt.org/g/KTjpbx 在优化和不优化的情况下都能正确编译(在 Godbolt 上使用 gcc4.1)
猜你喜欢
  • 2011-01-02
  • 1970-01-01
  • 2011-03-24
  • 2017-10-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多