0

Я пытаюсь использовать блокировку с двойным проверкой для поддержки массива биномиальных коэффициентов, но недавно я прочитал, что блокировка с двойной проверкой не работает. Эффективность чрезвычайно важна, поэтому использование volatile не является вариантом, если только внутри условных операторов. Я не вижу способа использовать статический класс с одноэлементным объектом (это часть структуры, и я не знаю, какие числа людей должны будут использовать эту функцию, поэтому я не могу догадаться, что такое максимум выбранное значение будет или будет ли функция вообще использоваться). Единственное, о чем я могу думать, это сделать все, что не статично, и настаивать на том, чтобы каждый поток, который должен использовать этот метод, создавал экземпляр объекта Select со своим собственным массивом. Похоже, это не обязательно.Двойная проверка блокировки для растущего массива биномиальных коэффициентов

public static final class Util{ 
/** 
* Static array of nCr values 
*/ 
public static long[][] nCr_arr; 

/** 
* Calculate binomial coefficient (n k) 
* 
* @param n 
*   n 
* @param k 
*   k 
* @return n choose k 
*/ 
public static long nCr(int n, int k) { 
    if (k < 0) 
     throw new ArithmeticException("Cannot choose a negative number"); 
    if (n < 0) { 
     if (k % 2 == 0) 
      return nCr(-n + k - 1, k); 
     else 
      return -nCr(-n + k - 1, k); 
    } 
    if (k > n) 
     return 0; 
    if (k > n/2) 
     k = n - k; 
    if (nCr_arr == null) { 
     synchronized (Util.class) { 
      if (nCr_arr == null) 
       nCr_arr = new long[n + 1][]; 
     } 
    } 
    if (nCr_arr.length <= n) { 
     synchronized (Util.class) { 
      if (nCr_arr.length <= n) { 
       long[][] newNCR = new long[n + 1][]; 
       System.arraycopy(nCr_arr, 0, newNCR, 0, nCr_arr.length); 
       nCr_arr = newNCR; 
      } 
     } 
    } 
    if (nCr_arr[n] == null) { 
     synchronized (Util.class) { 
      if (nCr_arr[n] == null) 
       nCr_arr[n] = new long[k + 1]; 
     } 
    } 
    if (nCr_arr[n].length <= k) { 
     synchronized (Util.class) { 
      if (nCr_arr[n].length <= k) { 
       long[] newNCR = new long[k + 1]; 
       System.arraycopy(nCr_arr[n], 0, newNCR, 0, 
         nCr_arr[n].length); 
       nCr_arr[n] = newNCR; 
      } 
     } 
    } 
    if (nCr_arr[n][k] == 0) { 
     if (k == 0) 
      nCr_arr[n][k] = 1; 
     else 
      nCr_arr[n][k] = nCr(n, k - 1) * (n - (k - 1))/k; 
    } 
    return nCr_arr[n][k]; 
} 
} 
+0

Я бы рекомендовал сделать это настоящим классом. Что произойдет, если кто-то вызовет ваш метод во второй раз с более крупным «n»? Массив будет инициализирован, но не будет достаточно большим. Не только это, если есть потоки, которые используют этот статический метод одновременно, у вас будут серьезные проблемы. –

+0

Кроме того, если вы не ожидаете одновременного использования, устраните этот синхронизированный блок, поскольку это уже не имеет значения, а затем тщательно документируйте это. Вам не нужно беспокоиться о блокировке вообще, если вы просто используете это в одном потоке. Но, как мой комментарий выше, если вы используете это с несколькими потоками, у вас много других проблем. –

+0

Простите, что вы пытаетесь расширить массив в следующем тесте. Здесь все еще есть проблемы параллелизма с последующими назначениями. –

ответ

0

В итоге я просто не стал статическим. Если поток должен получить значения nCr, он создает новый объект Coefficient и удерживает его.

0

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

if (nCr_arr == null) { 
    synchronized (Util.class) { 
     if (nCr_arr == null) 
      nCr_arr = new long[n + 1][]; 
    } 
} 

к этому:

synchronized (Util.class) { 
    if (nCr_arr == null) 
     nCr_arr = new long[n + 1][]; 
} 

небось влияние на производительность будет очень мала.

+0

Скорее всего, воздействие будет действительно высоким: вы блокируете каждое использование указателя, тогда как двойная блокировка имеет стоимость блокировки только в (редких) случаях, когда внешний, если сбой. –

+1

@dribeas: Скорее всего, если это не будет строго спорным. Недавние JVM не делают реальной блокировки в этом случае, так что незаконная синхронизация выполняется довольно быстро. Он должен будет сделать некоторые ориентиры в своей конкретной обстановке, чтобы узнать. –

+0

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

0

Вы уверены, что это необходимо для оптимизации? Профилировали ли вы запущенный код и обнаружили, что один замок стоит слишком дорого?

0

Или перепишите свой код, используя новый API параллельного Java http://download.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/ReadWriteLock.html и получите блокировку записи только в случае необходимости.

+0

К сожалению, в документах API говорится: «Если операции чтения слишком короткие, накладные расходы на реализацию блокировки чтения и записи [...] могут доминировать над стоимостью выполнения, особенно, поскольку многие блокировки чтения и записи по-прежнему сериализуют все потоки через небольшая часть кода » Между этим ответом и решением ConcurrentHashMap я ясно вижу, что Java пытается лучше всего поддерживать такую ​​поддержку, и в этом случае это недостаточно. Поэтому лучшим решением является просто дать отдельный массив для каждого потока. – dspyz

0

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

Вместо этого я бы потребовал, чтобы пользователь вашей библиотеки вручную указывал, сколько коэффициентов ей нужно во время инициализации. В качестве альтернативы, я бы прекомпретировал больше, чем пользователь, который, вероятно, понадобится - вы можете поместить все nCk для n < 1000 в 1 Мб памяти.

PS: Могу ли я предложить вам использовать рекурсивную формулу для вычисления коэффициента?

c[n][k] = c[n-1][k-1] + c[n-1][k] 

Это не имеет большого значения, но зачем использовать в сложной формуле, когда вам нужно Pascals Triangle?

+0

Любая формула будет действовать, и вы совершенно правы, что я мог бы просто предварительно вычислить все разумные значения, которые могут понадобиться в этом случае. Однако в будущем мне может понадобиться создать трехмерный массив для всех многочленных коэффициентов [x^k] (1-x^h)^n/(1-x), а затем количество, которое я могу легко прекомпомировать, будет значительно понижаться. Учитывая выбор между настройкой произвольного максимума и созданием объектов выбора, для которых потоки отвечают за отслеживание, я предпочитаю использовать объекты «Выбрать». – dspyz

0

Похоже, что вы построили кеш результатов при их вычислении, поэтому вы можете использовать параллельную карту для хранения результатов, создавая ключ, который объединяет 2 значения int в один длинный.

import java.util.concurrent.ConcurrentHashMap; 
import java.util.concurrent.ConcurrentMap; 

public final class Util { 
    /** 
    * Static array of nCr values 
    */ 
    private static final ConcurrentMap<Long,Long> CACHE = 
     new ConcurrentHashMap<Long, Long>(); 

    /** 
    * Calculate binomial coefficient (n k) 
    * 
    * @param n 
    *   n 
    * @param k 
    *   k 
    * @return n choose k 
    */ 
    public static long nCr(int n, int k) { 
     if (k < 0) 
      throw new ArithmeticException("Cannot choose a negative number"); 
     if (n < 0) { 
      if (k % 2 == 0) 
       return nCr(-n + k - 1, k); 
      else 
       return -nCr(-n + k - 1, k); 
     } 

     if (k > n) 
      return 0; 
     if (k > n/2) 
      k = n - k; 

     final long key = (n << 32L) + k; 

     Long value = CACHE.get(key); 
     if (value != null) { 
      return value.longValue(); 
     } 

     long result; 

     if (k == 0) 
      result = 1; 
     else 
      result = nCr(n, k - 1) * (n - (k - 1))/k; 

     CACHE.put(key, result); 

     return result; 
    } 
} 
+0

Глядя на источник ConcurrentHashMap, я вижу, что метод get() начинается с: if (count! = 0) {// ... Но счет считается изменчивым, поэтому для этого требуется прямой доступ к памяти каждый раз , В целом, java.util больше об удобстве, чем эффективности, поэтому использование вещей из рамки коллекций может иметь скрытые недостатки. – dspyz

+0

Это не всегда так и зависит от архитектуры процессора. На Intel волатильные чтения довольно дешевы (доступ к кеш-памяти L1 или L2). Его гораздо дешевле, чем конкурированный синхронизированный блок, в котором есть 4 потенциальных случая в вашем коде. Избежать этих классов, вероятно, является преждевременная оптимизация - особенно если вы пытаетесь создать правильное решение.На вашем коде не хватает взаимодействия на пути чтения, который будет активировать событие до/после семантики модели памяти, что может означать, что некоторые изменения могут быть не видны (это недостаток в двойном проверенном шаблоне без изменчивости). –

0

Оригинальный код имеет слишком много условий гонки. Для начала вы не можете обновить энергонезависимый nCr_arr и надеяться на работу идиомы с двойной проверкой. Объявление об этом волатильности полностью разрушает цель кеша. Правильный код не должен использовать синхронизацию вообще, кроме CAS.

CHM - очень плохой выбор здесь (также CHM не хорошо масштабируется).(Кроме того, использование Long как ключа не очень хорошее b/c того, как работает valueOf, он не может быть правильно встроен в точку доступа, так как он не всегда создает объект, а поле конечного значения не помогает)

Если кто-либо (все еще) интересуется, как сделать код, отбросить заметку. Cheers