【问题标题】:List of maps: efficient implementations地图列表:高效的实现
【发布时间】:2013-11-07 13:12:13
【问题描述】:

我有创建和使用集合的代码,例如:

List<Map<String, Object>> tableData;

这个地图列表由 n 个地图填充,每个地图代表数据库中的一行。每一行都表示为字段名称和字段对应的对象之间的映射(在这种情况下类型无关)。可能缺少某些字段。字段数,m 总是远小于行数(n ≈ 10000 × m)。我需要多次重复使用同一个集合来读取所有行,所以我不能只使用某种惰性迭代器。

是否有有效的数据结构来存储它? Guava 提供了一个Table 集合,但这似乎不符合要求。我正在考虑创建一个界面,例如:

interface TableData{
  int size();
  Map<String, Object> get(int i);
  // ... (interators, etc.)
}

然后创建一个使用 Map&lt;String,List&lt;Object&gt;&gt; 的实现,这样我只实例化 m 列表而不是 n 地图,并且仅在需要时动态创建地图,但我想知道是否有更通用的数据结构。

谢谢

【问题讨论】:

  • 能否请您说明为什么 Guava Table(例如 HashBasedTable)不适合您?根据stackoverflow.com/questions/1526596,您的内存表大小不是很大,每行拥有 HashMap 与 ArrayList 的开销是一个非常接近 3 的常数因子,您能否提供对内存限制的粗略估计?
  • 如果数据的总大小为 1-200 MB,则因子 3 不可忽略。我正在尝试探索所有选项。 HashBasedTable 的效率还有多少?

标签: java collections guava space-efficiency


【解决方案1】:

我进行了一些测试(无论如何都不是结论性的,但非常具有指示性)来确定不同 List&lt;Map&lt;String, Object&gt;&gt; 实现的内存占用。基线是 Java 的 ArrayList&lt;&gt;,元素是 Guava 的 ImmutableMap 的实例。

我比较的实现如下:

  1. 基于Map&lt;String,List&lt;Object&gt;&gt; 使用HashMap 和ArrayLists 实现;
  2. 基于List&lt;Object[]&gt; 使用ArrayList 实现;
  3. Guava 的HashBasedTable&lt;Integer,String,Object&gt;;
  4. Guava 的ArrayTable&lt;Integer,String,Object&gt;;

我的测试包括生成 n 个随机行,每个行都有 m 列和 k 的“填充因子”,其中填充因子为定义为每行包含所有列的值的概率。为简单起见,这些值是使用 Apache Commons RandomStringUtils 生成的长度为 l 的随机字符串。

但是让我们来看看结果。 n = 200000, m = 50, l = 10 和 k 在 (1.0, 7.5, 0.5)我得到以下内存占用占基线的百分比:

    | k = 1.0  | k = 0.75 | k = 0.5  |
----------------------------------------
1.  |     71 % |     71 % |     71 % |
2.  |     71 % |     72 % |     73 % |
3.  |    111 % |    107 % |    109 % |
4.  |     71 % |     73 % |     76 % |

我尝试将 n 减少到 20000,结果大致相同。

我发现上面的结果很有趣。首先,看起来没有太大的改进空间超过基线的 70%。其次,我惊喜地发现高效的 Guava 的 ArrayTable 与这个问题中提出的两种实现一样好。我会继续挖掘更多,但我倾向于解决方案 1。

谢谢

【讨论】:

  • 很好的研究。这对我很有帮助。
【解决方案2】:

首先请确定你确实需要优化。

假设平均不超过 50% 的列丢失,List&lt;Object[]&gt; 是明显的赢家:

class TableDataImpl implements TableData {
    private List<Object[]> data;
    private Map<String, Integer> columnNameToIndexMap;

    public Map<String, Object> get(int i) {
        return new ArrayMap(data.get(i));
    }

    private class ArrayMap implements Map<String, Object> {

        private Object[] row;

        ArrayMap(Object[] row) {
            this.row = row;
        }

        public Object get(String key) {
            Integer index = columnNameToIndexMap.get(key);
            if (index==null) return null;
            return row[index];
       }

       // all the other Map stuff... a lot of code!
    }
}

我不会说它简单,所以请确保您确实需要优化。

否则,假设平均不超过 95% 的列丢失,应该做一个稍微复杂一点的构造:对于每一行,使用本地开发的 BitSet (long[]) 来存储列存在。这样,您只会浪费Object[] 中的一个位,而不是整个条目(32 位或 64 位)。

这更加复杂,所以请确保您确实需要优化。

假设许多行共享同一组列,您可以在每一行中存储columnNameToIndexMap。

【讨论】:

  • 第一个想法是一种有趣的方法。一个带有 n 数组的集合(加上一个字段名称映射)。我要去探索一下。
【解决方案3】:

如果我有这么大的数据,我担心我会得到 OOM,那么我不会找到一个最佳的数据结构来保存这些数据,我会寻找如何使用 SIMD 并行性或类似 Map-Reduce 的东西。无论您如何优化数据结构,您总是会耗尽内存空间。例如,如果您确实找到了适用于特定机器配置的最佳数据结构,它可能仍然无法在 RAM 稍小的机器中工作。

但是,如果您仍想坚持当前的方法,为什么不能对数据进行规范化,以便可以通过 'Null' 表示缺少的字段。因此,当您读取数据并创建地图时,为什么不为缺少的字段添加“null”?这样你至少不需要像 hashmap 这样的键值数据结构,你可以 lise List&lt;List&lt;Object&gt;&gt;

【讨论】:

  • 这是一个有趣的想法,我已经想到了,但这样我仍然有 n 个集合,而不仅仅是 m。
【解决方案4】:

好吧,如果一次将所有表数据保存在内存中很重要,那么存储数据结构的方向(作为映射列表或列表映射)不会有太大区别)。地图列表显然更直观,所以我会保留它。

如果您担心对象创建和清理的效率,我建议您使用对象池。以下是它可能如何工作的基本概念:

public class TableRowPool {

    private static final int INITIAL_CAPACITY = 10000;

    private Queue<Map<String, Object>> mapObjects;

    public TableRowPool() {
        mapObjects = new LinkedList<Map<String, Object>>();
        for(int i = 0; i < INITIAL_CAPACITY; i++) {
            mapObjects.add(new HashMap<String, Object>());
        }
    }

    public Map<String, Object> getTableRowObject() {
        if(mapObjects.size() == 0) {
            mapObjects.add(new HashMap<String, Object>());
        }
        return mapObjects.remove();
    }

    public void returnTableRowObject(Map<String, Object> obj) {
        mapObjects.add(obj);
    }

}

LinkedList 作为一个队列执行得很好,所以对象检索会很快。如果您希望它动态增长,它还可以快速添加新对象。但是,您可能需要更改数据结构,具体取决于它是否需要是线程安全的。

要使用对象池,您可以执行以下操作:

//Load data
while((row = getResultSetRow()) != null) {
    Map<String, Object> rowObj = tableRowPool.getTableRowObject();
    //Fill in data
    myRows.add(rowObj);
}

//... Do all your business logic ...

//Cleanup
for(Map<String, Object> rowObj : myRows) {
    tableRowPool.returnTableRowObject(rowObj);
}
myRows = null;

【讨论】:

  • 感谢您的回答。我曾经探索过使用对象池的可能性。我发现这种方法不安全,因为它给客户端带来了很大的负担:返回对象并且不再使用它。这是一个很大的责任,根据我的经验,很容易违反。此外,它通过强制客户端了解集合的实现,打破了关注点分离原则。
  • 我关心对象的创建,但最重要的是内存使用。将这么多集合保存在内存中的开销有时会使应用程序不堪重负并引发 OOM 错误。如果我保留旧集合,我将有一个包含 n 个映射的列表,每个映射平均包含 m 个元素,而如果我创建一个列表映射,现在我在一张地图中有 m 个大小为 n 的列表,这可以减少保存在内存中的集合。如果表的大小和字段数已知,则可以进一步简化集合(参见 Guava ArrayTable)。
  • 我会假设它是您保存在内存中的实际数据,而不是将成为内存猪的数据结构。是的,您可以节省一些费用(如果您知道行数,然后有一个列名到数组索引的 single 映射,则可以在获得结果集时创建一个预先确定大小的二维数组) .但是,尝试将所有行都放在内存中可能是不合理的。根据查询的不同,再次发出它并在迭代它们时在每一行上运行业务逻辑通常并不是那么昂贵。
  • 对于预先确定大小的数组的想法,应该是这样的。 Object[][] data = new Object[rows][columns]; 和 Map&lt;String, Integer&gt; columnMap = new HashMap&lt;String, Integer&gt;()。然后迭代列名并将它们添加到columnMap。接下来迭代行并分配数组值,如下所示:data[i][columnMap.get(dbRow.columnName())] = dbRow.value();。
  • 您好,谢谢您的回复。不幸的是,数据并非来自数据库,并且事先不知道行数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-04
  • 1970-01-01
  • 2010-11-07
  • 2017-03-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多