RFR 8017540: Improve multi-threaded contention behavior of BigInteger.toString() radix conversion cache

Peter Levart peter.levart at gmail.com
Thu Jun 27 08:51:45 UTC 2013


Hi,

Expanding on the idea of building single growing linked list of values 
and treating the array just as index for quick access which can be 
re-created at any time without synchronization or volatile publication, 
here's the attempt:


     private static class Node {
         final BigInteger value;
         Node next;
         Node(BigInteger value) { this.value = value; }
     }

     private static final Node[] powerCache;
     private static final Node[][] powerCacheIndex;
     private static final int POWER_CACHE_LINE_CHUNK = 16;

     static {
         powerCache = new Node[Character.MAX_RADIX + 1];
         for (int i = Character.MIN_RADIX; i <= Character.MAX_RADIX; i++) {
             powerCache[i] = new Node(BigInteger.valueOf(i));
         }
         powerCacheIndex = new Node[Character.MAX_RADIX + 1][];
     }

     private static BigInteger getRadixConversionCache(int radix, int 
exponent) {
         Node[] cacheLine = powerCacheIndex[radix];
         if (cacheLine != null && exponent < cacheLine.length) { // 
cache line is long enough
             Node node = cacheLine[exponent];
             if (node != null) {
                 return node.value;
             }
             return fillCacheLine(cacheLine, powerCache[radix], exponent);
         }
         else { // we need to extend / create cache line
             cacheLine = new Node[(exponent / POWER_CACHE_LINE_CHUNK + 
1) * POWER_CACHE_LINE_CHUNK];
             BigInteger result = fillCacheLine(cacheLine, 
powerCache[radix], exponent);
             powerCacheIndex[radix] = cacheLine; // install new line
             return result;
         }
     }

     private static BigInteger fillCacheLine(Node[] cacheLine, Node 
node0, int exponent) {
         Node node = cacheLine[0] = node0;
         for (int i = 1; i <= exponent; i++) {
             Node nextNode;
             synchronized (node) {
                 nextNode = node.next;
                 if (nextNode == null) {
                     nextNode = new Node(node.value.square());
                     node.next = nextNode;
                 }
             }
             cacheLine[i] = node = nextNode;
         }
         return node.value;
     }



What do you think?

Regards, Peter


On 06/27/2013 09:27 AM, Peter Levart wrote:
> On 06/27/2013 08:37 AM, Peter Levart wrote:
>> Hi,
>>
>> This version seems correct. Maybe just replace double volatile read 
>> at length re-check with a single one:
>>
>>
>>     private static BigInteger getRadixConversionCache(int radix, int 
>> exponent) {
>>         BigInteger[] cacheLine = powerCache[radix]; // volatile read
>>         if (exponent < cacheLine.length)
>>             return cacheLine[exponent];
>>
>>         int oldLength = cacheLine.length;
>>         cacheLine = Arrays.copyOf(cacheLine, exponent + 1);
>>         for (int i = oldLength; i <= exponent; i++)
>>             cacheLine[i] = cacheLine[i - 1].pow(2);
>>
>>         BigInteger[][] pc = powerCache; // volatile read again
>>         if (exponent >= pc[radix].length) {
>>             pc = pc.clone();
>>             pc[radix] = cacheLine;
>>             powerCache = pc; // volatile write, publish
>>         }
>>         return cacheLine[exponent];
>>     }
>>
>>
>> I like this, since it tries to avoid overwriting larger cacheLines 
>> with smaller ones when unexistent exponents are requested 
>> concurrently for same radix. This is good, since computation for 
>> larger exponents takes quadratically longer time (as Alan Eliasen 
>> points out) and possibility of concurrent threads stomping on each 
>> other increases. Re-reading and cloning powerCache reference at end 
>> of computation also takes care of cacheLines for other radixes that 
>> might have expanded while the computation was taking place. So the 
>> only overhead remaining is when concurrent threads are uselessly 
>> computing same results at same time. To avoid this, locking and 
>> waiting would have to be introduced which would *complicate things*.
>>
>> Regards, Peter
>
> On the other hand, it doesn't complicate thing too much. So if this 
> extra CPU time proves to be a problem, here's a variation of above 
> code which prevents multiple threads from calculating the same result 
> at the same time, by serializing computation with precise granularity:
>
>     private static class Node {
>         final BigInteger value;
>         Node next;
>         Node(BigInteger value) { this.value = value; }
>     }
>
>     private static volatile Node[][] powerCache;
>
>     static {
>         powerCache = new Node[Character.MAX_RADIX + 1][];
>         for (int i = Character.MIN_RADIX; i <= Character.MAX_RADIX; i++) {
>             powerCache[i] = new Node[]{new Node(BigInteger.valueOf(i))};
>         }
>     }
>
>     private static BigInteger getRadixConversionCache(int radix, int 
> exponent) {
>         Node[] cacheLine = powerCache[radix]; // volatile read
>         if (exponent < cacheLine.length)
>             return cacheLine[exponent].value;
>
>         int oldLength = cacheLine.length;
>         cacheLine = Arrays.copyOf(cacheLine, exponent + 1);
>         Node prevNode = cacheLine[oldLength - 1];
>         for (int i = oldLength; i <= exponent; i++) {
>             Node node;
>             synchronized (prevNode) {
>                 node = prevNode.next;
>                 if (node == null) {
>                     node = new Node(prevNode.value.pow(2));
>                     prevNode.next = node;
>                 }
>             }
>             cacheLine[i] = prevNode = node;
>         }
>
>         Node[][] pc = powerCache; // volatile read again
>         if (exponent >= pc[radix].length) {
>             pc = pc.clone();
>             pc[radix] = cacheLine;
>             powerCache = pc; // volatile write, publish
>         }
>         return cacheLine[exponent].value;
>     }
>
>
> This variation differs only in a subtelty. It wraps each BigInteger 
> with a Node, which has a link to next node in chain. Computation of 
> next node is serailized using previous node as a lock. So although 
> Nodes can end up in several arrays (which are used as index for quick 
> access), there is only a single growing chain of Nodes per radix and 
> each Node is computed by single thread.
>
> Regards, Peter
>
>>
>> On 06/26/2013 08:13 PM, Brian Burkhalter wrote:
>>> So do we have consensus on this version?
>>>
>>> Thanks for the lively "conversation."
>>>
>>> Brian
>>>
>>> On Jun 26, 2013, at 12:05 AM, Aleksey Shipilev wrote:
>>>
>>>> Yes, like that.
>>>>
>>>> -Aleksey
>>>>
>>>> On 26.06.2013, at 10:53, Dmitry Nadezhin 
>>>> <dmitry.nadezhin at gmail.com> wrote:
>>>>
>>>>>> We could check for the existing cacheLine.length right before 
>>>>>> installing
>>>>> the new one
>>>>>
>>>>> Do you mean something like this ?
>>>>>
>>>>> BigInteger getRadixConversionCache(int radix, int exponent) {
>>>>> BigInteger[] cacheLine = powerCache[radix]; // volatile read
>>>>> if (exponent < cacheLine.length)
>>>>>     return cacheLine[exponent];
>>>>>
>>>>> int oldLength = cacheLine.length;
>>>>> cacheLine = Arrays.copyOf(cacheLine, exponent + 1);
>>>>> for (int i = oldLength; i < exponent + 1; i++)
>>>>>     cacheLine[i] = cacheLine[i - 1].square();
>>>>>
>>>>> if (exponent >= powerCache[radix].length) { // volatile read again
>>>>>    BigInteger[][] pc = Arrays.copyOf(powerCache);
>>>>>    pc[radix] = cacheLine;
>>>>>    powerCache = pc; // volatile write, publish
>>>>> }
>>>>> return cacheLine[exponent];
>>>>> }
>>
>




More information about the core-libs-dev mailing list