Как реализовать атомарное сравнение и обмен с помощью инструкции сборки cmpxchg8b в c?

Я пытаюсь реализовать связанный список без блокировки. Для этого мне нужно реализовать атомарную инструкцию сравнения и замены, используя встроенную сборку в C. Я знаю, что мне нужно использовать инструкцию cmpxchg8b для x86, однако я не могу этого сделать.

Моя структура узла выглядит следующим образом:

typedef struct node
{
    int data;
    struct node * next;
    struct node * backlink;
}node_lf;

Указатель struct node * next также содержит дополнительную информацию в последних 2 битах (биты метки и флага).

Сравнение и обмен, которые мне нужно реализовать, выглядят следующим образом:

node_lf *cs (cs_arg * address, cs_arg *old_val, cs_arg *new_val )
{
    node_lf *value = (address->node)->next;
    if ((address->node->next) == old_val->node)
    {
        (address->node)->next = new_val->node;
    }
    return value;
}

Структура cs_arg выглядит следующим образом:

typedef struct csArg
{
    node_lf * node;
}cs_arg;

Моя реализация:

static inline node_lf* cs(cs_arg * address, cs_arg *old_val, cs_arg *new_val)
{
    node_lf * value;
    __asm__ __volatile__("lock cmpxchg8b %0; "
                :"=q"(value)
                :"a"(address->node->next), "b"(old_val->node), "c"(new_val->node)
                :"memory");
    return value;
}

Это дает сообщение об ошибке: ../list.c:86: Error: operand type mismatch for 'cmpxchg8b' make: *** [list.o] Error 1

Пожалуйста, помогите мне решить эту проблему.


person Sumant    schedule 14.06.2016    source источник
comment
Вы смотрели stackoverflow.com/q/37739170/2189500? Он делает примерно то же самое. И, по удивительному совпадению, он также называет свои структуры данных.   -  person David Wohlferd    schedule 14.06.2016
comment
@DavidWohlferd: Да, я просмотрел этот вопрос, прежде чем задавать его. Думаю, мы оба имеем в виду одну и ту же бумагу. Мне нужно знать, что не так в инструкции cmpxchg8b. Есть ли справочное руководство, где я могу прочитать о cmpxchg8b с использованием встроенного ассемблера?   -  person Sumant    schedule 14.06.2016
comment
Если встроенный ассемблер не является требованием класса, его следует по возможности избегать. В gcc есть встроенные модули (см. __sync_bool_compare_and_swap), есть std::atomic и т. д. Что касается документов, мой первый google попадание кажется достаточным. FYI %0 относится к 0-му параметру (также известному как value). Эта (неинициализированная) переменная, вероятно, не та, с которой вы собирались сравнивать. Сообщение об ошибке связано с использованием q, который на i386 представляет собой 8-битное значение (прокрутите вниз до i386).   -  person David Wohlferd    schedule 15.06.2016
comment
Я написал некоторый код, показывающий, как использовать cmpxchg8b, используя встроенный ассемблер, который может быть полезен. Посетите stackoverflow.com/a/37825052/2189500.   -  person David Wohlferd    schedule 15.06.2016
comment
@DavidWohlferd Спасибо за ваш ценный вклад. Однако я не могу использовать __sync_bool_compare_and_swap, так как он возвращает логическое значение. Мне нужно старое значение address->node->next в качестве возвращаемого значения. Будет ли приведенный выше код работать, если я изменю =q на =d и %0 на %1, поскольку мне нужно сравнить address->node->next?   -  person Sumant    schedule 15.06.2016
comment
Итак... Что-то вроде __sync_val_compare_and_swap ?   -  person David Wohlferd    schedule 15.06.2016
comment
@DavidWohlferd: `__sync_val_compare_and_swap` возвращает целое число и требует (int *) в качестве адреса. Я пытался задать этот вопрос здесь link   -  person Sumant    schedule 15.06.2016
comment
ЛОЖЬ. __sync_val_compare_and_swap поддерживает целочисленные типы, включая int, long long и __int128.   -  person David Wohlferd    schedule 16.06.2016


Ответы (1)


Давайте сделаем это шаг за шагом. Прежде всего, попробуем использовать __sync_val_compare_and_swap в простом случае:

#include <stdio.h>

typedef struct node
 {
    int data;
    struct node * next;
    struct node * backlink;
 }node_lf;

int cs1(node_lf *address, int old_val, int new_val)
{
    int *p2 = &address->data;
    int p3 = __sync_val_compare_and_swap(p2, old_val, new_val);  

    return p3;
}

int main()
{
   node_lf n;

   n.data = 17;

   int res = cs1(&n, 1, 2);
   printf("Old Value: %d  Cur Value: %d\n", res, n.data);

   res = cs1(&n, 17, 2);
   printf("Old Value: %d  Cur Value: %d\n", res, n.data);
}

Итак, код здесь довольно прост. Мы манипулируем node.data. Мы начинаем с инициализации его значением 17. Затем мы пытаемся выполнить cas, чтобы изменить его на 2. Однако при первом вызове мы даем неверное значение для oldval (1 против 17). В результате __sync_val_compare_and_swap (правильно!) не выполняет своп, а возвращает текущее значение. Второй вызов, который дает правильное старое значение, выполняет замену и возвращает старое значение.

Итак, при запуске получаем:

Old Value: 17  Cur Value: 17 <- Didn't change the value
Old Value: 17  Cur Value: 2  <- Did change the value

Достаточно просто, но не совсем то, что вы ищете. Давайте немного приблизим его:

#include <stdio.h>

typedef struct node
 {
    int data;
    struct node * next;
    struct node * backlink;
 }node_lf;

typedef struct csArg
{
    node_lf * node;
}cs_arg;

node_lf *cs2(node_lf *address, const cs_arg *old_val, const cs_arg *new_val)
{
    unsigned long long *p2 = (unsigned long long *)&address->next;
    unsigned long long p3 = __sync_val_compare_and_swap (p2, (unsigned long long)old_val->node, (unsigned long long)new_val->node);  

    return (node *)p3;
}

int main()
{
   node_lf n;
   cs_arg oldval, newval;

   n.next = (node *)18;
   oldval.node = (node *)1;
   newval.node = (node *)2;

   node *res2 = cs2(&n, &oldval, &newval);
   printf("Old Value: %p  Cur Value: %p\n", res2, n.next);

   oldval.node = (node *)18;
   res2 = cs2(&n, &oldval, &newval);
   printf("Old Value: %p  Cur Value: %p\n", res2, n.next);
}

В этом случае мы пытаемся обновить node.next, используя узлы в 2 cs_args. Здесь важно отметить, что, поскольку __sync_val_compare_and_swap работает с целочисленными типами, нам нужно привести указатели к типу int соответствующего размера. Предполагается, что мы используем 64-битный компилятор, поэтому указатели имеют размер 8 байт (то же самое, что и unsigned long long).

Здесь нет никакой необходимости использовать структуру cs_arg. Использование узла * должно работать нормально. Но вы использовали его, так что, по-видимому, здесь есть какой-то долгосрочный план.

Запустив это, мы видим:

Old Value: 0000000000000012  Cur Value: 0000000000000012  <- Didn't change the value
Old Value: 0000000000000012  Cur Value: 0000000000000002  <- Did change the value

Обратите внимание, что можно увеличить это значение до 16 байт (используя __int128 и cmpxchg16b), если вы используете x64. Тем не менее, есть некоторые хитрости, о которых следует знать. Например, данные должны быть выровнены по 16 байтам (что НЕ является node.next).

Теперь, проделав все это, ваше требование о том, что вам «нужно старое значение address->node->next в качестве возвращаемого значения», кажется подозрительным. Глядя на приведенный выше код, как определить, что cas не сработал? Если он работает, он возвращает 18, а если нет, он возвращает 18. Поскольку это то, что вы сказали, что хотели, это то, что я пытался предоставить.

Но я предполагаю, что вы хотите знать, сработало ли это (возможно, чтобы попробовать еще раз). В таком случае, возможно, что-то более похожее на это:

#include <stdio.h>

typedef struct node
 {
    int data;
    struct node * next;
    struct node * backlink;
 }node_lf;

bool cs2(node_lf *address, const node *old_val, const node *new_val)
{
    unsigned long long *p2 = (unsigned long long *)&address->next;
    return __sync_bool_compare_and_swap (p2, (unsigned long long)old_val, (unsigned long long)new_val);
}

int main()
{
   node_lf n;
   node *oldval, *newval;

   n.next = (node *)18;
   oldval = (node *)1;
   newval = (node *)2;

   bool res2;

   do {

      printf("Trying to change %p from %p to %p: ", n.next, oldval, newval);
      res2 = cs2(&n, oldval, newval);
      printf("Worked: %d  Cur Value: %p\n", res2, n.next);
      if (res2)
         break;

      oldval = n.next;
   } while (1);
}

Когда вы выйдете из цикла, oldval будет тем, что было раньше (должно быть, иначе cas не сработает), newval будет тем, что на самом деле было записано. Обратите внимание, что если бы это действительно было многопоточным, нет никакой гарантии, что newval будет таким же, как n.next, поскольку другой поток уже мог появиться и изменить его снова.

Хотя использование ассемблера может сэкономить вам одну или две инструкции, выигрыш с точки зрения удобочитаемости, ремонтопригодности, переносимости и т. д. почти наверняка стоит затрат.

Если только это не классный проект, где учителю требуется встроенный ассемблер. В этом случае посмотрите https://stackoverflow.com/a/37825052/2189500.

person David Wohlferd    schedule 16.06.2016
comment
@DavidWohlfred: Спасибо, пост отвечает на все, что мне нужно. - person Sumant; 17.06.2016
comment
Для некоторых связанных ответов на какой-то (замечательно) похожий код, проверьте stackoverflow.com/a/37849201/2189500 - person David Wohlferd; 17.06.2016