【问题标题】:A faster way to generate a vector of UUIDs in R在 R 中生成 UUID 向量的更快方法
【发布时间】:2018-04-09 16:48:12
【问题描述】:

下面的代码大约需要 15 秒来生成一个包含 10k 个 UUID 的向量。我需要生成 1M 或更多,我计算这将需要 15 * 10 * 10 / 60 分钟,或大约 25 分钟。有没有更快的方法来实现这一点?

library(uuid)
library(dplyr)
start_time <- Sys.time()
temp <- sapply( seq_along(1:10000), UUIDgenerate )
end_time <- Sys.time()
end_time - start_time  

# Time difference of 15.072 secs

基本上,我正在为 R 寻找一种方法,该方法能够实现此处描述的 Java 性能提升:Performance of Random UUID generation with Java 7 or Java 6

它们应该符合 RFC 4122,但其他要求是灵活的。

【问题讨论】:

  • 你为什么要回答自己?
  • @JeanRostan 回答你自己的问题并没有错,事实上这是鼓励
  • 据我所知,这在这个网站上很常见。事实上,您会在可以提问的同一屏幕上看到该选项作为复选框。就我而言,在编写问题时,我通过在我的代码中尝试一些新的东西来发现这个答案。然后我决定无论如何发布答案,以防它对其他人有用。如果我误解了网站规则和最佳实践,其他任何人都可以随时在这里发表评论。
  • @HongOoi 确实,我不知道,因为我是新手。谢谢鲍比。参考链接:stackoverflow.com/help/self-answer
  • all(seq_along(1:10000) == 1:10000) ... 不需要seq_along,它只会减慢您的速度(大约使执行时间翻倍)。

标签: r uuid


【解决方案1】:

前面的底线:不,目前没有办法在不损害唯一性的核心前提的情况下加速大量具有uuid 的 UUID 的生成。 (即使用uuid。)

事实上,您使用use.time=FALSE 的建议会产生严重的不良后果(在Windows 上)。见下文。

有可能在规模上获得更快的性能,但 uuid 不行。见下文。

uuid 在 Windows 上

uuid::UUIDgenerate 的性能应该考虑到操作系统。更具体地说,随机性的来源。看性能很重要,是的,在哪里:

library(microbenchmark)
microbenchmark(
  rf=replicate(1000, uuid::UUIDgenerate(FALSE)),
  rt=replicate(1000, uuid::UUIDgenerate(TRUE)),
  sf=sapply(1:1000, function(ign) uuid::UUIDgenerate(FALSE)),
  st=sapply(1:1000, function(ign) uuid::UUIDgenerate(TRUE))
)
# Unit: milliseconds
#  expr       min        lq     mean   median       uq      max neval
#    rf  8.675561  9.330877 11.73299 10.14592 11.75467  66.2435   100
#    rt 89.446158 90.003196 91.53226 90.94095 91.13806 136.9411   100
#    sf  8.570900  9.270524 11.28199 10.22779 12.06993  24.3583   100
#    st 89.359366 90.189178 91.73793 90.95426 91.89822 137.4713   100

...所以使用use.time=FALSE 总是更快。 (我将sapply 示例与您的答案代码进行比较,以表明replicate 永远不会变慢。除非您觉得出于某种原因需要数字参数,否则请在此处使用replicate。)

但是,有一个问题:

R.version[1:3]
#          _                 
# platform x86_64-w64-mingw32
# arch     x86_64            
# os       mingw32           
length(unique(replicate(1000, uuid::UUIDgenerate(TRUE))))
# [1] 1000
length(unique(replicate(1000, uuid::UUIDgenerate(FALSE))))
# [1] 20

鉴于 UUID 旨在每次调用时都是唯一的,这令人不安,并且是 Windows 上随机性不足的症状。 (WSL 是否为此提供了出路?另一个研究机会......)

uuid 在 Linux 上

为了比较,在非 Windows 平台上的结果相同:

microbenchmark(
  rf=replicate(1000, uuid::UUIDgenerate(FALSE)),
  rt=replicate(1000, uuid::UUIDgenerate(TRUE)),
  sf=sapply(1:1000, function(ign) uuid::UUIDgenerate(FALSE)),
  st=sapply(1:1000, function(ign) uuid::UUIDgenerate(TRUE))
)
#  Unit: milliseconds
#   expr       min       lq     mean   median       uq       max neval
#     rf 20.852227 21.48981 24.90932 22.30334 25.11449  74.20972   100
#     rt  9.782106 11.03714 14.15256 12.04848 15.41695 100.83724   100
#     sf 20.250873 21.39140 24.67585 22.44717 27.51227  44.43504   100
#     st  9.852275 11.15936 13.34731 12.11374 15.03694  27.79595   100

R.version[1:3]
# _
# platform x86_64-pc-linux-gnu
# arch     x86_64
# os       linux-gnu
length(unique(replicate(1000, uuid::UUIDgenerate(TRUE))))
# [1] 1000
length(unique(replicate(1000, uuid::UUIDgenerate(FALSE))))
# [1] 1000

(我对use.time=FALSE 在 linux 上花费的时间是 windows 上的两倍……有点好奇……)

使用 SQL 服务器生成 UUID

如果您可以访问 SQL 服务器(几乎可以肯定...参见 SQLite ...),那么您可以通过使用 服务器的 UUID 生成实现来处理这个规模问题,认识到存在一些细微差别。

(旁注:有“V4”(完全随机)、“V1”(基于时间)和“V1mc”(基于时间并包括系统的 MAC 地址)UUID。uuid 给出 V4 如果@ 987654339@ 和 V1 否则,编码系统的 mac 地址。)

一些性能比较在 windows 上(所有时间都以秒为单位):

#         n  uuid postgres sqlite sqlserver
# 1     100     0     1.23   1.13      0.84
# 2    1000  0.05     1.13   1.21      1.08
# 3   10000  0.47     1.35   1.45      1.17
# 4  100000  5.39     3.10   3.50      2.68
# 5 1000000 63.48    16.61  17.47     16.31

SQL 的使用有一些开销,在大规模使用时不会花费很长时间来克服。

  • PostgreSQL 需要uuid-ossp 扩展,可以安装

    CREATE EXTENSION "uuid-ossp"
    

    一旦安装/可用,您可以生成n UUID:

    n <- 3
    pgcon <- DBI::dbConnect(...)
    DBI::dbGetQuery(pgcon, sprintf("select uuid_generate_v1mc() as uuid from generate_series(1,%d)", n))
    #                                   uuid
    # 1 53cd17c6-3c21-11e8-b2bf-7bab2a3c8486
    # 2 53cd187a-3c21-11e8-b2bf-dfe12d92673e
    # 3 53cd18f2-3c21-11e8-b2bf-d3c64c6ad73f
    

    存在其他 UUID 函数。 https://www.postgresql.org/docs/9.6/static/uuid-ossp.html

  • SQLite 的功能有限,但这种 hack 对于 V4 样式的 UUID(长度 n)来说已经足够了:

    sqlitecon <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") # or your own
    DBI::dbGetQuery(sqlitecon, sprintf("
            WITH RECURSIVE cnt(x) as (
              select 1 union all select x+1 from cnt limit %d
            )
            select (hex(randomblob(4))||'-'||hex(randomblob(2))||'-'||hex(randomblob(2))||'-'||hex(randomblob(2))||'-'||hex(randomblob(6))) as uuid
            from cnt", n))
    #                                   uuid
    # 1 EE6B08DA-2991-BF82-55DD-78FEA48ABF43
    # 2 C195AAA4-67FC-A1C0-6675-E4C5C74E99E2
    # 3 EAC159D6-7986-F42C-C5F5-35764544C105
    

    将其格式化为相同的格式需要一点点痛苦,充其量只是一种精细。如果不坚持这种格式,您可能会发现性能上的小幅提升。)

  • SQL Server 需要临时创建一个表(使用newsequentialid()),在其中生成一个序列,提取自动生成的 ID,然后丢弃该表。有点过头了,特别是考虑到使用 SQLite 的易用性,但是 YMMV。 (没有提供代码,它不会增加太多。)

其他注意事项

除了执行时间和足够的随机性之外,关于使用非连续 UUID 指示性能影响的数据库表,还有各种讨论(暂时未引用)。这与索引页面等有关,超出了此答案的范围。

但是,假设这是真的……假设大约在同一时间插入的行(时间相关)通常被分组在一起(直接或子分组),那么保持同一天是一件好事在同一数据库索引页中具有 UUID 键的数据,因此 V4(完全随机)UUID 可能会降低大型组(和大型表)的数据库性能。出于这个原因,我个人更喜欢 V1 而不是 V4。

其他(仍未引用)讨论认为在 UUID 中包含可直接追踪的 MAC 地址是对内部信息的轻微破坏。出于这个原因,我个人倾向于 V1mc 而不是 V1。

(但我还没有办法用RSQLite 很好地做到这一点,所以我依赖于附近有postgresql。幸运的是,我使用postgresql 来处理我保留一个实例的其他事情Windows 上的泊坞窗。)

【讨论】:

  • 答案看起来很完整,谢谢!你有使用字节码编译器的经验吗?你认为它在这里会有所帮助吗? channel9.msdn.com/Events/useR-international-R-User-conferences/…
  • 我用过它,但我不相信它会给你使用uuid 带来任何加速:字节编译器适用于 R 代码,但 C 代码几乎总是更快。 UUIDgenerate 在 Windows 上如此缓慢的原因是可用熵低。尽管您可能会在(例如)Rcpp 中快速想出一些东西,但libuuid 库付出了巨大的努力来确保唯一性,这是您的自定义代码必须复制的东西。我认为加速的最佳位置是libuuid(不是uuid)允许一次对n UUID 的请求而不是循环。 (以及更好的 Windows 熵。)
【解决方案2】:

提供选项use.time 将显着加快进程。它可以设置为TRUEFALSE,以确定UUID 是否基于时间。在这两种情况下,它都会比不指定此选项快得多。

对于 10k UUID,

library(uuid)
library(dplyr)

start_time <- Sys.time()
temp <- sapply( seq_along(1:10000), function(ign) UUIDgenerate(FALSE) )
end_time <- Sys.time()
end_time - start_time
# 10k: 0.01399994 secs

start_time <- Sys.time()
temp <- sapply( seq_along(1:10000), function(ign) UUIDgenerate(TRUE)  )
end_time <- Sys.time()
end_time - start_time
# 10k: 0.01100016 secs

即使扩展到 100M,运行时间仍然比原来的 15 秒更快。

start_time <- Sys.time()
temp <- sapply( seq_along(1:100000000), function(ign) UUIDgenerate(FALSE)  )
end_time <- Sys.time()
end_time - start_time
# 100M: 1.154 secs

start_time <- Sys.time()
temp <- sapply( seq_along(1:100000000), function(ign) UUIDgenerate(TRUE)  )
end_time <- Sys.time()
end_time - start_time
# 100M: 3.7586 secs

【讨论】:

  • 支持是use.time=FALSE而不是use.time, FALSE
  • args(UUIDgenerate) sapply 不合适。如果您坚持使用它,匿名函数将使其可行,如 r2e 的回答。您的代码也应该给出错误(在新会话中尝试),但不会因为您在全局环境中有一个名为 use.time 的对象,显然。
  • unused argument 错误是有道理的:你做错了。问题是您隐含地提供了数字(来自1:100..)作为第一个参数。如果你这样做sapply(1:100, function(ign) UUIDgenerate(FALSE)),它会起作用的。请参阅我的答案中的示例代码。 (这可能是对匿名函数和命名函数使用sapply 的误解。)
  • 鲍比,意识到sapply(1:10, func)sapply(1:30, function(i) func(i)) 相同。如果您想忽略序列号,您的三个选项是:(1)编写函数以显式忽略它,如果使用预先编写的函数则不可能; (2) 用sapply(1:10, function(ign) func()) 明确忽略它;或 (3) 使用replicate(10, func())。第三个更清楚地表明它忽略数字的意图,因此通常是首选。 (注意replicate 在内部调用sapply。)
  • 旁注:(1)不需要library(dplyr); (2) 可以使用system.time(...) 代替Sys.time();...;Sys.time() 三重调用。
猜你喜欢
  • 1970-01-01
  • 2015-12-29
  • 1970-01-01
  • 1970-01-01
  • 2019-11-20
  • 2017-10-17
  • 2023-03-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多