【问题标题】:Performing 100k iterations in less than 4 seconds在 4 秒内执行 100k 次迭代
【发布时间】:2017-05-06 14:10:25
【问题描述】:

我需要迭代一个HashMap,其中包含10^5 轮次的整数。我随机生成整数。然后,我对该整数执行所需的算术运算。之后,我正在检查 HashMap 是否包含此 Integer => 如果它包含此整数,我会递增该整数并重新检查 HashMap 是否包含新整数,直到该整数不存在于 HashMap 中。如果它不包含整数,我将整数添加到 HashMap。

我在下面添加了我的代码。 'start' 和 'end' cmets 之间的代码耗时太长。 如果我在“开始”和“结束”之间comment 这段代码,它会在不到一秒的时间内执行。 所以,Random.nextInt()HashMap.containsKey() 中没有消耗时间

MyProgram.java

import java.util.HashMap;
import java.util.Random;

public class MyProgram {

    public static void main(String[] args) {

        long total = 0;
        int randomInt;
        int count = 100000;
        int divider = 3;
        Random random = new Random();   

        HashMap<Integer, Integer> map = new HashMap<>();

        for(int i=0; i < count; i++){

            randomInt = random.nextInt(count);

            // start
            int value1 = randomInt / divider;
            int value2 = (randomInt % divider != 0) ? 1 : 0;
            randomInt = value1 + value2;
            // end

            while(map.containsKey(randomInt)){
                randomInt++;
            }

            map.put(randomInt, 0);  // don't care about value
            total += randomInt;
        }

        System.out.println("Total : " + total);
    }
}

此实现需要 30 多秒。

我可以使用 List、Arrays、ArrayLists 等。如果您认为它们很快的话。请举例说明。

【问题讨论】:

  • 您不需要检查地图是否已经包含键,因为您不关心值。您可以尝试继续推送随机键,直到您的地图中有 count 键。不确定这会更快,但试一试
  • 需要时间的可能是整数的装箱。也许您应该创建一个可变整数类型并使用它。此外,您可以使用 randomInt = (random.nextInt(count) + divider - 1) / divider 而不是您的 value1value2 计算
  • 我的意思是,如果您的键 (randomInt) 是 17,并且它已经存在于地图中,并且如果您再次将 17 放入地图中,那么地图将替换针对 17 保存的任何值,并且用新值替换它。您为所有键保持相同的值 0。另外,我刚刚运行了您的代码,并观察到 ​​while 循环一直在占用。我把它注释掉了,程序在 ~ 2 秒内运行
  • 对你的性能损失的解释是你执行containsKey的次数,当数字重复时会更大,在执行那个算术时会更大。
  • @rupinderjeet 谢谢,但这是一种低劣的方法,您应该改用 BitSet。这就是我删除它的原因

标签: java performance time hashmap


【解决方案1】:

由于您不关心映射中存储的值,而只关心整数键,因此只需使用BitSet

  • 用适当大小的BitSet 替换地图:

    BitSet bits = new BitSet(count);
    
  • 使用nextClearBit 方法查找下一个未设置的位:

    randomInt = bits.nextClearBit(randomInt);
    
  • 然后设置那个位:

    bits.set(randomInt);
    

这对我来说完成得非常快(0.16 秒,在 Ideone):Ideone demo


我没有测量它,但我猜想OP的代码缓慢的主要原因是该行中的隐式对象创建:

while(map.containsKey(randomInt)){

因为地图包含引用,randomInt 必须装箱到Integer;只有 -128 到 127 范围内的整数才能保证被缓存,因此这将导致创建大量对象。 BitSet 避免创建对象,因为它对原始 ints 进行操作。

这在 Effective Java 2nd Ed 第 5 条:“避免创建不必要的对象”中进行了讨论(寻找它说“非常慢的程序!”)。

【讨论】:

  • 您可以阅读 cmets 的其他答案并仍然说这行得通吗?
  • 您指的是哪些特定的 cmets 和答案?演示链接不符合您的预期吗?请注意,这实际上并没有在功能上改变代码,它只是使用了不同的数据结构,您正在使用的操作在该数据结构上效率更高。
  • @PatrickParker 确实可以。所以呢?这不是 OP 代码的功能更改。
  • @rupinderjeet 构造函数传递了一个初始大小,BitSet 可以根据需要增长。这是正确的答案。
  • 你是对的。你的两个解决方案都对我有用。感谢您参考书上的相关主题。
【解决方案2】:

问题在于内部 while(map.containsKey(randomInt)) 循环运行至少 30 亿次(查看下面粘贴的输出中 Entered 变量的值),因为与计算后生成的最终数字发生巨大冲突.

int value1 = randomInt / divider;
int value2 = (randomInt % divider != 0) ? 1 : 0;
randomInt = value1 + value2;

此代码在循环100000 次时会生成许多常见值,并且与自动装箱一起这可能会导致性能问题。

您可以检查发生冲突时 while 循环执行的时间量。

int randomInt;
int count = 100000;
int divider = 3;
long entered = 0;
Random random = new Random();   

HashMap<Integer, Integer> map = new HashMap<>();

for(int i=0; i < count; i++){

    randomInt = random.nextInt(count);

    // start
    int value1 = randomInt / divider;
    int value2 = (randomInt % divider != 0) ? 1 : 0;
    randomInt = value1 + value2;
    // end

    while(map.containsKey(randomInt)){
        entered++;
        randomInt++;
    }

    map.put(randomInt, 0);  // don't care about value
    total += randomInt;
}

System.out.println("Total : " + total);
System.out.println("Entered : " + entered);

输出:

Total : 4999950000
Entered : 3335662228

因此,您应该重新审视value1+value2 的逻辑,而不是仅使用random.nextInt(count) 并将计数指定为您想要生成的范围。

randomInt = random.nextInt(count);
while(map.containsKey(randomInt)){
    randomInt = random.nextInt(count);
}

【讨论】:

  • 您的解决方案不是一个选项。我使用random.nextInt() 进行测试。所有randomIntcount 值都在文件中预定义。如果存在,我可以将文件中的randomInt 增加一个。
【解决方案3】:

一些琐碎的事情:

  • 如果您不关心值,请使用 HashSet 而不是 Map。
  • 将cmets之间的部分替换为:randomInt = (randomInt + divider - 1) / divider

这些都是小事,但 cmets 之间的部分不太可能是导致性能问题的主要因素。

因为您要生成 0

import java.util.HashSet;
import java.util.Random;

class Interval {
  int min, max;
  public Interval(int min, int max) {
    this.min = min;
    this.max = max;
  }

  public String toString() {
    return "[" + min + "," + max + "]";
  }
}

public class MyProgram {
  private static void checkConsistency(Interval[] intv) {
    for(int i=0; i<intv.length; i++) {
      Interval v = intv[i];
      if (v != null && (i < v.min || i > v.max)) {
        throw new Error(i + " -> " + v);
      }
    }
  }

  public static void main(String[] args) {
    long total = 0;
    int randomInt;
    int count = 100000;
    int divider = 3;
    Random random = new Random();
    HashSet<Integer> hs = new HashSet<>();
    Interval[] data = new Interval[count];

    for(int i=0; i < count; i++){
        randomInt = random.nextInt(count);
        // start
        randomInt = (randomInt + divider -1) / divider;
        // end
        Interval intv = data[randomInt];
        if (intv != null) {
          randomInt = intv.max + 1;
        }
        int idx = randomInt < count ? randomInt : count - 1;
        hs.add(randomInt);
        Interval pre = randomInt > 0 ? data[randomInt-1] : null;
        Interval post = randomInt < count-1 ? data[randomInt+1] : null;
        if (pre == null && post == null) {
          data[idx] = new Interval(randomInt, randomInt);
        } else if (pre != null && post != null) {
          if (pre.max-pre.min < post.max-post.min) {
            for (int j=pre.min; j <= pre.max; j++) {
              data[j] = post;
            }
            data[idx] = post;
          } else {
            for (int j=post.min; j <= post.max; j++) {
              data[j] = pre;
            }
            data[idx] = pre;
          }
          data[idx].min = pre.min;
          data[idx].max = post.max;
        } else if (pre != null) {
          data[idx] = pre;
          data[idx].max = randomInt;
        } else {
          data[idx] = post;
          data[idx].min = randomInt;
        }
        // just for verifying consistency
        checkConsistency(data);
        total += randomInt;
    }
      System.out.println("Total : " + total);
  }
}

【讨论】:

  • 我在另一个答案中选择了 BitSet 方法。但是,谢谢。
【解决方案4】:

如果您不关心这些值(当然,如果您的程序中不需要它),请使用HashSet 而不是HashMap。它与 HashMap 键列表具有相同的行为。不能有重复的值。

这意味着如果您的集合已经包含值 102 并且生成的下一个随机整数也是 102,则将其添加到集合中将无济于事。 (对于 Hashmap,它只会替换键/值对,但如果您的值始终为 0,则不会引起注意)。

因此,您不需要检查您的集合是否包含新的随机 int,这部分变得无用:

while(map.containsKey(randomInt)){
            randomInt++;
}

另外,这部分是耗时的,因为 containsKey 在地图上循环,因此当你的地图中有很多键时,你可以循环很长时间(如果增加的值也在列表中会发生什么?和下一个增量值?等等?)。 这应该会让你的编程速度更快。

现在您仍然可以通过删除 value1value2 的声明来优化您的代码,但时间消耗比您的 whileloop 低得多,因此可能不会有太大的不同。

最后,您的代码应如下所示:

int randomInt;
int count = 100000;
int divider = 3;
Random random = new Random();   

HashSet<Integer> set = new HashSet<>();

while(set.size()<count){

    randomInt = random.nextInt(count);

    // start
    map.add((randomInt/divider) + ((randomInt % divider != 0) ? 1 : 0));
    // end
}

【讨论】:

  • 如果HashSet中包含randomInt,则不会插入到HashSet中。我需要 HashSet 来增加 randomInt 并重试插入。如果它再次包含新的randomInt,我需要再次增加randomInt 并重新尝试插入。依此类推.. 直到递增 randomInt 不存在于 HashSet 中。
  • 您应该在问题中写下您需要来增加值。正如目前所写的那样,它看起来更像是在描述修复尝试而不是需求。
  • 因为这是一个HashMap containsKey() 不会遍历映射中的所有值,只会遍历那些恰好在同一个哈希桶中的值。因此,HashMap 的适当大小可能会带来一些性能改进......
【解决方案5】:

问题不在于除法代码本身,问题在于您在尝试插入map 之前为randomInt 生成了更小的范围,因此产生了更多的冲突。

如果您查看以下代码变体

        for(int i=0; i < count; i++){

            randomInt = random.nextInt(33333);

            // start
//            int value1 = randomInt / divider;
//            int value2 = (randomInt % divider != 0) ? 1 : 0;
//            randomInt = value1 + value2;
            // end

            while(map.containsKey(randomInt)){
                randomInt++;
            }

            map.put(randomInt, 0);  // don't care about value
            total += randomInt;
        }

您会发现它所花费的时间与执行除法的代码所用的时间大致相同。所以你应该考虑一下你的插入策略,看看你是否可以改进它。 (我不能帮助你,因为我还没有完全理解你想要用你的代码实现什么)。

【讨论】:

  • 你的意思是value1 + value2 是一个较小的值,它会产生很多冲突。正确的?但是,我需要对这个随机整数执行这个操作。
  • 问题不在于您正在对随机整数执行一些操作,而是您正在生成 666666 次保证冲突(很可能更多)。我想到了一个可能执行得更快的变体,我稍后会编辑帖子。
  • 好吧,我的想法带来了一些改进,但没有达到我希望的规模,所以我就放弃了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-23
  • 2014-10-21
  • 1970-01-01
相关资源
最近更新 更多