覆盖自定义序列的`drop`

时间:2012-10-13 06:01:35

标签: clojure override

简而言之:在Clojure中,有没有办法在我写的自定义序列类型上从标准序列API(在ISeq,IndexedSeq等任何接口上没有定义)重新定义函数?


1。巨大的数据文件

我有以下格式的大文件:

  • 包含条目数n的长(8个字节)
  • n个条目,每个条目由3个长整数(即24个字节)组成

2。自定义序列

我希望这些条目有一个序列。由于我通常不能同时将所有数据保存在内存中,并且我希望快速顺序访问它,我写了一个类似于以下的类:

(deftype DataSeq [id
                  ^long cnt
                  ^long i
                  cached-seq]
  clojure.lang.IndexedSeq

  (index [_]     i)
  (count [_]     (- cnt i))
  (seq   [this]  this)
  (first [_]     (first cached-seq))
  (more  [this]  (if-let [s (next this)] s '()))

  (next [_] (if (not= (inc i) cnt)
              (if (next cached-seq)
                (DataSeq. id cnt (inc i) (next cached-seq))
                (DataSeq. id cnt (inc i)
                          (with-open [f (open-data-file id)]
                             ; open a memory mapped byte array on the file
                             ; seek to the exact position to begin reading
                             ; decide on an optimal amount of data to read
                             ; eagerly read and return that amount of data
                          ))))))

主要思想是提前读取列表中的一堆条目,然后从该列表中使用。每当完全使用缓存时,如果存在剩余条目,则从新缓存列表中的文件中读取它们。就这么简单。

要创建这样一个序列的实例,我使用一个非常简单的函数,如:

(defn ^DataSeq load-data [id]
  (next (DataSeq. id (count-entries id) -1 [])))
; count-entries is a trivial "open file and read a long" memoized

正如您所看到的,数据格式使我能够非常简单有效地实施count

3。 drop可以是O(1)

本着同样的精神,我想重新实现drop 。这些数据文件的格式允许我重新实现O(1)中的drop(而不是标准O(n)),如下所示:

  • 如果减少剩余的缓存项目,只需从缓存中删除相同的金额并完成;

  • 如果丢弃超过cnt,则只返回空列表。

  • 否则,只需找出数据文件中的位置,向右跳到该位置,然后从那里读取数据。

我的难点在于,drop的实现方式与countfirstseq等不同。后一种函数调用类似命名的静态方法RT反过来调用我上面的实现,而前者drop不会检查调用它的序列的实例是否提供自定义实现。

显然,我可以提供一个名为drop的函数,它完全符合我的要求,但这会强迫其他人(包括我未来的自己)记住每次使用它而不是drop单次,这很糟糕。

因此,问题是:是否可以覆盖drop的默认行为?

4。解决方法(我不喜欢)

在撰写这个问题时,我只想出了一个可能的解决方法:让阅读变得更加懒散。自定义序列只保留一个索引并推迟读取操作,这只有在调用first时才会发生。问题是我需要一些可变状态:第一次调用first会导致一些数据被读入缓存,所有后续调用都将从此缓存返回数据。 next上会有类似的逻辑:如果有缓存,只需next;否则,不要费心填充它 - 将在再次调用first时完成。

这可以避免不必要的磁盘读取。然而,这仍然不是最优的 - 它仍然是O(n),它很容易就是O(1)。

无论如何,我不喜欢这种解决方法,我的问题仍然存在。有什么想法吗?

感谢。

1 个答案:

答案 0 :(得分:1)

目前,我实施了上述的解决方法。它的工作原理是将读数推迟到(first)的第一次调用,该调用将数据存储在本地可变缓存中。

请注意,此版本使用unsynchronized-mutable (以避免每次调用firstnextmore时出现易失性读取-write第一次调用first)。换句话说:不要在线之间分享。为了使其成为线程安全的,请改用volatile-mutable(这会导致性能损失很小)。它仍然可能导致不同线程对同一数据进行多次读取。为避免这种情况,请更改回unsynchronized-mutable,并确保在阅读或写入字段(locking this ...)时使用cache

编辑:经过一些(非严格的)测试后,(locking this ...)引入的开销似乎与磁盘上不必要的读取引入的开销类似(请注意,我正在阅读来自快速SSD,可能已经缓存了部分数据)。因此,现在(以及我的特定硬件)最好的线程安全解决方案是使用易失性缓存。

(deftype DataSeq [id
                  ^long cnt
                  ^long i
                  ^{:unsynchronized-mutable true} cache]
  clojure.lang.IndexedSeq

  (index [_]    i)
  (count [_]    (- cnt i))
  (seq   [this] this)
  (more  [this] (if-let [s (.next this)] s '()))
  (next  [_]    (if (not= (inc i) cnt)
                  (DataSeq. id cnt (inc i) (next cache))))
  (first [_]
    (when-not (seq cache)
      (set! cache
            (with-open [f (open-data-file id)]
              ; open a memory mapped byte array on the file
              ; seek to the exact position to begin reading
              ; decide on an optimal amount of data to read
              ; eagerly read and return that amount of data
            )))
    (first cache)))

还有什么困扰我的是,我必须使用可变状态来阻止drop(即,“脱离,你无用的数据”)从磁盘读取......