Атомный усилитель C++

Я переписываю алгоритм на C++ AMP и только что столкнулся с проблемой атомарной записи, точнее atomic_fetch_add, которая, по-видимому, предназначена только для целых чисел?

Мне нужно добавить double_4 (или, если нужно, float_4) атомарным способом. Как мне добиться этого с помощью атомарности С++ AMP?

Является ли лучшее/единственное решение иметь переменную блокировки, которую мой код может использовать для управления записью? На самом деле мне нужно выполнять атомарную запись для длинного списка выходных двойников, поэтому мне по сути нужна блокировка для каждого вывода.

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

РЕДАКТИРОВАТЬ: Спасибо за уже предоставленные быстрые ответы. У меня есть быстрое обновление моего вопроса.

Я предпринял следующую попытку блокировки, но кажется, что когда один поток в варпе преодолевает блокировку, все остальные потоки в том же варпе просто следуют за ним. Я ожидал, что первый поток деформации получит блокировку, но я должен что-то упустить (обратите внимание, что прошло довольно много лет с тех пор, как я был cuda, поэтому я просто стал тупым)

parallel_for_each(attracting.extent, [=](index<1> idx) restrict(amp)
{
   .....
   for (int j = 0; j < attracted.extent.size(); j++)
   {
      ...
      int lock = 0; //the expected lock value
      while (!atomic_compare_exchange(&locks[j], &lock, 1));
      //when one warp thread gets the lock, ALL threads continue on
      ...
      acceleration[j] += ...; //locked write
      locks[j] = 0; //leaving the lock again
   }
});

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


person JoeTaicoon    schedule 20.06.2014    source источник
comment
по следующей ссылке нет проблем с добавлением поплавков или двойников, какая проблема у вас была? pic.dhe.ibm.com/infocenter/lnxpcomp/v121v141/   -  person RRR    schedule 20.06.2014
comment
Атомные операции обычно не поддерживаются изначально. Хотя использование T* в документах, кажется, предполагает обратное, вам, вероятно, нужно прочитать переменную (из указателя с типом), изменить ее и выполнить сравнение-обмен, опять же, для целочисленного указателя с типом. Когда они говорят T, они, скорее всего, имеют в виду любое целое число T, а не любое T.   -  person Damon    schedule 20.06.2014
comment
Что касается вашего редактирования - вы дали каждому потоку свой собственный замок? Как тогда они будут взаимоисключающими?   -  person Leeor    schedule 20.06.2014
comment
Блокировки глобальные. Одна блокировка на спорную позицию записи. Следовательно, каждый поток должен пытаться получить одну и ту же блокировку j перед записью в j.   -  person JoeTaicoon    schedule 20.06.2014


Ответы (3)


Все атомарные операции добавления предназначены только для целочисленных типов. Вы можете делать все, что хотите, без блокировок, используя 128-битные операции CAS (сравнение и обмен) хотя и для float_4 (я предполагаю, что это 4 числа с плавающей запятой), но нет 256-битных операций CAS, которые вам понадобятся для double_4. Что вам нужно сделать, так это иметь цикл, который атомарно считывает float_4 из памяти, выполняет добавление с плавающей запятой обычным способом, а затем использует CAS для проверки и замены значения, если оно является исходным (и цикл, если нет, то есть какой-то другой поток изменил значение между чтением и записью). Обратите внимание, что 128-разрядный CAS доступен только в 64-разрядных архитектурах и что ваши данные должны быть правильно выровнены.

person JarkkoL    schedule 20.06.2014
comment
Сначала казалось расточительным зацикливаться на чтении, сравнении и записи, как вы предложили, но я думаю, что это так же хорошо, как зацикливаться при попытке получить блокировку. Однако мне интересно, что вы думаете об этой реализации: while (!atomic_compare_exchange(&locks[j], &lock, 1)); ускорение[j] += согл; // запись по спорному адресу locks[j] = 0; //снимаем блокировку Где у меня есть array_View такой же длины, как и array_view ускорения...? - person JoeTaicoon; 20.06.2014
comment
@JoeTaicoonless То, что я предложил, является распространенным шаблоном, используемым в программировании без блокировки. Разница в том, что с реализацией без блокировок, как я предложил, в системе всегда происходит некоторый прогресс. В предложенной вами реализации система может просто остановить исполняемый поток в середине добавления, а другие потоки вынуждены ждать завершения (т.е. снятия блокировки). - person JarkkoL; 20.06.2014
comment
поэтому, если блокировка уже занята другим потоком, вы можете приостановить поток с помощью операции yield вместо ожидания ожидания - person RRR; 20.06.2014
comment
Вы могли бы, но это все равно мешает прогрессу в системе. В зависимости от вашего случая это может быть неосуществимым способом избежать остановки. - person JarkkoL; 20.06.2014

если критический код короткий, вы можете создать свой собственный замок, используя атомарные операции:

int lock = 1;

while(__sync_lock_test_and_set(&lock, 0) == 0) // trying to acquire lock
{
 //yield the thread or go to sleep
} 

//critical section, do the work

// release lock
lock = 1;

преимущество заключается в том, что вы экономите на блокировках ОС.

person RRR    schedule 20.06.2014
comment
Я не думаю, что это доступно в С++ AMP? - person JoeTaicoon; 20.06.2014
comment
чего нет в C++ AMP? вы можете использовать любой метод сравнения и замены вместо __sync_lock_test_and_set - person RRR; 20.06.2014
comment
Извините, просто предположил, что вы имели в виду конкретный библиотечный метод. Как оказалось, ваше предложение — это именно то, что я только что попробовал. Пожалуйста, смотрите редактирование в моем вопросе для деталей. - person JoeTaicoon; 20.06.2014

На этот вопрос как таковой ответили другие, и ответ заключается в том, что вам нужно самостоятельно обрабатывать двойные атомы. В библиотеке нет для него функции.

Я также хотел бы подробно рассказать о своем собственном редактировании на случай, если другие придут сюда с такой же неисправной блокировкой.

В следующем примере моя ошибка заключалась в том, что я не понял, что при сбое обмена фактически изменилось ожидаемое значение! Таким образом, первый поток ожидает, что блокировка будет равна нулю, и запишет в нее 1. Следующий поток будет ожидать 0 и не сможет записать единицу, но затем биржа запишет единицу в переменную, содержащую ожидаемое значение. Это означает, что в следующий раз, когда поток попытается выполнить обмен, он ожидает 1 в блокировке! Это он получает, а затем думает, что получает блокировку.

Я совершенно не знал, что &lock получит 1 при неудачном обмене!

parallel_for_each(attracting.extent, [=](index<1> idx) restrict(amp)
{
   .....
   for (int j = 0; j < attracted.extent.size(); j++)
   {
      ...
      int lock = 0; //the expected lock value

      **//note that, if locks[j]!=lock then lock=1
      //meaning that ACE will be true the next time if locks[j]==1
      //meaning the while will terminate even though someone else has the lock**
      while (!atomic_compare_exchange(&locks[j], &lock, 1));
      //when one warp thread gets the lock, ALL threads continue on
      ...
      acceleration[j] += ...; //locked write
      locks[j] = 0; //leaving the lock again
   }
});

Кажется, что исправить это сделать

parallel_for_each(attracting.extent, [=](index<1> idx) restrict(amp)
{
   .....
   for (int j = 0; j < attracted.extent.size(); j++)
   {
      ...
      int lock = 0; //the expected lock value

      while (!atomic_compare_exchange(&locks[j], &lock, 1))
      {
          lock=0; //reset the expected value
      };
      //when one warp thread gets the lock, ALL threads continue on
      ...
      acceleration[j] += ...; //locked write
      locks[j] = 0; //leaving the lock again
   }
});
person JoeTaicoon    schedule 20.06.2014
comment
Лучшее решение - использовать atomic_exchange, который не меняет ожидания - person JoeTaicoon; 23.06.2014