【问题标题】:Scalability issue with HashMap in Multithreaded Multicore system多线程多核系统中 HashMap 的可扩展性问题
【发布时间】:2011-11-08 12:23:08
【问题描述】:

我在从 hashmap 读取数据时面临可扩展性问题。我的机器有 32 个核心,每个核心有 2 个超线程(总共 64 个 CPU)和 64 GB RAM。 从 HashMap 读取数据并进行算术计算时,我发现从 16 个线程开始性能下降,但在仅执行算术运算时,它会按预期进行缩放。

请在下面找到测试结果:

从HashMap中读取并进行算术运算:

线程数 |所用时间(秒)=> 1 | 85, 2 | 93, 4 | 124, 8 | 147, 16 | 644

只执行算术运算:

线程数 |所用时间(秒)=> 1 | 25, 2 | 32, 4 | 35, 8 | 41, 16 | 65, 32 | 108, 40 | 112, 64 | 117, 100 | 158

同时添加代码块供参考:

import java.util.*;

import java.util.concurrent.*;

import java.lang.*;

public class StringCallable2
{

//  private static final long   size    = 500000L;
    private static final long   size    = 1000000L;
//  private final static HashMap <Long,Long>map = new HashMap<Long, Long>();

//  private static long[] array = new long[(int) size];
    public static class StringGenCallable implements Callable
    {
        int count;
        public StringGenCallable(int count)
        {
            this.count = count;
        }

        public Long call()
        {

            //Random rand = new Random();
//          System.out.println("Thread " + count + " started test");
            long sum = 20;
            // do a CPU intensive arithmetic operation; no Input Output
            // operations, object creations or floating point arithmetic

            for (long i = 0; i < size; i++)
            {
                //int numNoRange = rand.nextInt((int)(size-1));
                //long numNoRange = i;
                // Long long1 = map.get((long)i);
                //Long long1 = array[(int)i];
                sum = i + 19 * sum;
            }
//          System.out.println("Finished " + count);

            return sum;
        }
    }

    public static void main(String args[]) 
    {
        try
        {
        System.out.println("Starting");
        // for (long i = 0; i < size; i++)
        // {
            //array[(int)i] = System.currentTimeMillis();
        //  map.put(i, System.currentTimeMillis());
        // }
        int sizt = Integer.valueOf(args[0]);
        long curtime = System.currentTimeMillis();
        ExecutorService pool = Executors.newFixedThreadPool(sizt);
        Set<Future<Integer>> set = new HashSet<Future<Integer>>();
        for (int i = 0; i < sizt; i++)
        {
            Callable<Integer> callable = new StringGenCallable(i);
            Future<Integer> future = pool.submit(callable);
            set.add(future);
        }

        long sum = 0;
        for (Future<Integer> future : set)
        {
            future.get();
        }

        System.out.println("Number of threads : "+sizt);
        long finsihtime = System.currentTimeMillis();
        System.out.println("Total Time Taken : " + (finsihtime - curtime)+" ms");
        pool.shutdown();
        // System.exit(sum);
        }
        catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        catch (Error e) {
            // TODO: handle exception
            e.printStackTrace();
        }
        catch (Throwable e) {
            // TODO: handle exception
            e.printStackTrace();
        }
    }

}

【问题讨论】:

  • 哎呀。你忘记了问题。
  • 这里的问题是什么?众所周知,锁争用会损害可伸缩性。无论如何,在您的情况下,您可以尝试ConcurrentHashMap,它针对多线程使用进行了优化。
  • 如果你用的是java5+,那就试试java.util.ConcurrentHashMap,这个类比较适合

标签: java multithreading


【解决方案1】:

对于具有这种多处理级别的应用程序,您应该使用ConcurrentHashMap。我会重新设计以融入这种变化,然后重新审视性能。

我还会仔细考虑您可以有效使用多少线程。很容易将“添加更多线程”视为性能灵丹妙药,但事实并非如此。您可以通过限制线程数并将当前共享的数据结构变为ThreadLocal 来获得更多改进,以减少数据共享以及由此产生的争用和上下文切换。

在本例中,即使假设您拥有该进程的整个机器,拥有超过 64 个线程也会使进程运行速度越来越慢,因为工作项完全受 CPU 限制。

在现实世界的应用程序中,工作单元可能比您在此处的工作单元更复杂或更长时间运行。小心不要从对你的硬件来说是一个相当微不足道的每线程工作单元得出太多结论。关键是相对于更复杂的工作负载,这里的线程管理开销与执行的工作相比被放大了。在更复杂的工作负载中,HashMap 中查找的可见效果可能会消失,而性能看起来更像您的预期。

【讨论】:

    【解决方案2】:

    从您注释掉的代码的外观来看,高开销似乎来自自动装箱。对于每个map.get((long)i),您可能会分配一个新的Long 对象。分配很快,但没那么快。

    无论您有一个线程还是多个线程,这都适用。但是,对于许多线程来说,内存带宽可能比 CPU 更重要。

    Long.valueOf 的实现允许为相同的值返回相同的Long 实例,这很可能适用于小值。“转义分析”的应用程序也可以从堆中删除Long。)

    【讨论】:

      【解决方案3】:

      起初我怀疑这是因为 HashMap 案例在每次查找时都会创建一个对象。

      但是在测试之后(见下文),我认为问题在于获得对缓存的有效访问变得越来越困难。

      import gnu.trove.TLongLongHashMap;
      
      import java.util.HashMap;
      import java.util.concurrent.Callable;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      import java.util.concurrent.TimeUnit;
      
      /**
       * @author peter.lawrey
       */
      public class HashMapPerfMain {
          public static final int REPEATS = 10000;
      
          public static void main(String... args) throws InterruptedException {
              int runLength = 10 * 1000;
              HashMap<Long, Long> hashMap = new HashMap<Long, Long>();
              TLongLongHashMap troveMap = new TLongLongHashMap();
              long[] array = new long[runLength];
              for (long i = 0; i < runLength; i++) {
                  long now = System.nanoTime();
                  hashMap.put(i, now);
                  troveMap.put(i, now);
                  array[((int) i)] = now;
              }
      
              for (int i = 0; i < 3; i++) {
                  timeHashMap(hashMap);
                  timeTroveMap(troveMap);
                  timeArray(array);
              }
          }
      
          private static void timeHashMap(final HashMap<Long, Long> map) throws InterruptedException {
              System.out.printf("%-16s ", map.getClass().getSimpleName());
              for (int t = 1; t <= Runtime.getRuntime().availableProcessors(); t *= 2) {
                  long start = System.nanoTime();
                  ExecutorService es = Executors.newFixedThreadPool(t);
                  for (int i = 0; i < t * REPEATS; i++)
                      es.submit(new Callable<Long>() {
                          @Override
                          public Long call() throws Exception {
                              long sum = 20;
                              for (long key = 0; key < map.size(); key++)
                                  sum = sum * 19 + map.get(key);
                              return sum;
                          }
                      });
                  es.shutdown();
                  es.awaitTermination(10, TimeUnit.MINUTES);
                  long time = System.nanoTime() - start;
                  System.out.printf("%d | %.3f ", t, time / 1e9);
              }
              System.out.println();
          }
      
          private static void timeTroveMap(final TLongLongHashMap map) throws InterruptedException {
              System.out.printf("%-16s ", map.getClass().getSimpleName());
              for (int t = 1; t <= Runtime.getRuntime().availableProcessors(); t *= 2) {
                  long start = System.nanoTime();
                  ExecutorService es = Executors.newFixedThreadPool(t);
                  for (int i = 0; i < t * REPEATS; i++)
                      es.submit(new Callable<Long>() {
                          @Override
                          public Long call() throws Exception {
                              long sum = 20;
                              for (long key = 0; key < map.size(); key++)
                                  sum = sum * 19 + map.get(key);
                              return sum;
                          }
                      });
                  es.shutdown();
                  es.awaitTermination(10, TimeUnit.MINUTES);
                  long time = System.nanoTime() - start;
                  System.out.printf("%d | %.3f ", t, time / 1e9);
              }
              System.out.println();
          }
      
              private static void timeArray(final long [] array) throws InterruptedException {
                  System.out.printf("%-16s ", array.getClass().getSimpleName());
              for (int t = 1; t <= Runtime.getRuntime().availableProcessors(); t *= 2) {
                  long start = System.nanoTime();
                  ExecutorService es = Executors.newFixedThreadPool(t);
                  for (int i = 0; i < t * REPEATS; i++)
                      es.submit(new Callable<Long>() {
                          @Override
                          public Long call() throws Exception {
                              long sum = 20;
                              for (int key = 0; key < array.length; key++)
                                  sum = sum * 19 + array[key];
                              return sum;
                          }
                      });
                  es.shutdown();
                  es.awaitTermination(10, TimeUnit.MINUTES);
                  long time = System.nanoTime() - start;
                  System.out.printf("%d | %.3f ", t, time / 1e9);
              }
              System.out.println();
          }
      }
      

      打印

      HashMap          1 | 0.904 2 | 0.863 4 | 0.913 8 | 1.832 
      TLongLongHashMap 1 | 0.568 2 | 0.566 4 | 0.572 8 | 1.048 
      long[]           1 | 0.092 2 | 0.091 4 | 0.090 8 | 0.093 
      HashMap          1 | 0.767 2 | 0.773 4 | 0.912 8 | 1.833 
      TLongLongHashMap 1 | 0.560 2 | 0.563 4 | 0.570 8 | 1.057 
      long[]           1 | 0.088 2 | 0.089 4 | 0.090 8 | 0.096 
      HashMap          1 | 0.758 2 | 0.774 4 | 0.911 8 | 1.828 
      TLongLongHashMap 1 | 0.565 2 | 0.564 4 | 0.568 8 | 1.056 
      long[]           1 | 0.088 2 | 0.089 4 | 0.090 8 | 0.093 
      

      数组访问非常有效,因为它线性扫描内存。 HashMap 倾向于将数据伪随机地排列在内存中,给缓存带来更大的负担。

      【讨论】:

        猜你喜欢
        • 2023-04-02
        • 1970-01-01
        • 2012-04-19
        • 2018-06-18
        • 2014-02-10
        • 2012-08-14
        • 1970-01-01
        • 2012-02-15
        • 1970-01-01
        相关资源
        最近更新 更多