为什么`(map digitalToInt) . show`这么快?

2024-03-07

转换非负数Integer其数字列表通常是这样完成的:

import Data.Char

digits :: Integer -> [Int]
digits = (map digitToInt) . show

我试图找到一种更直接的方法来执行任务,而不涉及字符串转换,但我无法想出更快的方法。

到目前为止我一直在尝试的事情:

基线:

digits :: Int -> [Int]
digits = (map digitToInt) . show

从 StackOverflow 上的另一个问题得到这个:

digits2 :: Int -> [Int]
digits2 = map (`mod` 10) . reverse . takeWhile (> 0) . iterate (`div` 10)

尝试推出我自己的:

digits3 :: Int -> [Int]
digits3 = reverse . revDigits3

revDigits3 :: Int -> [Int]
revDigits3 n = case divMod n 10 of
               (0, digit) -> [digit]
               (rest, digit) -> digit:(revDigits3 rest)

这个的灵感来自于showInt in Numeric:

digits4 n0 = go n0 [] where
    go n cs
        | n < 10    =  n:cs
        | otherwise =  go q (r:cs)
        where
        (q,r) = n `quotRem` 10

现在是基准。注意:我强制使用filter.

λ>:set +s
λ>length $ filter (>5) $ concat $ map (digits) [1..1000000]
2400000
(1.58 secs, 771212628 bytes)

这是参考。现在为digits2:

λ>length $ filter (>5) $ concat $ map (digits2) [1..1000000]
2400000
(5.47 secs, 1256170448 bytes)

That's 3.46时间更长。

λ>length $ filter (>5) $ concat $ map (digits3) [1..1000000]
2400000
(7.74 secs, 1365486528 bytes)

digits3 is 4.89时间变慢。只是为了好玩,我尝试仅使用 revDigits3 并避免reverse.

λ>length $ filter (>5) $ concat $ map (revDigits3) [1..1000000]
2400000
(8.28 secs, 1277538760 bytes)

奇怪的是,这甚至更慢,5.24慢几倍。

最后一张:

λ>length $ filter (>5) $ concat $ map (digits4) [1..1000000]
2400000
(16.48 secs, 1779445968 bytes)

This is 10.43时间变慢。

我的印象是,仅使用算术和缺点会优于涉及字符串转换的任何内容。显然,有些东西我无法理解。

那么有什么窍门呢?为什么是digits很快?

我正在使用 GHC 6.12.3。


鉴于我还无法添加评论,我将做更多工作并分析所有评论。我把分析放在最上面;不过,相关数据如下。 (注意:所有这些都是在 6.12.3 中完成的 - 还没有 GHC 7 魔法。)


分析:

版本1:show 对于整数来说非常好,尤其是像我们这样短的整数。实际上,在 GHC 中制作字符串往往是不错的;然而,读取字符串并将大字符串写入文件(或标准输出,尽管您不想这样做)是您的代码绝对可以抓取的地方。我怀疑为什么这么快背后的很多细节都是由于 Ints 显示中的巧妙优化。

版本2:编译时,这是其中最慢的一个。一些问题:反向论证的严格性。这意味着在计算下一个元素时,您无法从对列表的第一部分执行计算中受益;您必须计算所有它们,翻转它们,然后对列表的元素进行计算(即 (`mod` 10) )。虽然这看起来很小,但它可能会导致更大的内存使用量(请注意此处分配的 5GB 堆内存)和更慢的计算速度。 (长话短说:不要使用反向。)

版本3:还记得我刚才说过不要使用反向吗?事实证明,如果你把它拿出来,总执行时间会下降到 1.79 秒——仅比基线慢一点。这里唯一的问题是,当你深入了解数字时,你会以错误的方向构建列表的主干(本质上,你是通过递归“进入”列表,而不是“进入”列表)列表)。

版本 4:这是一个非常巧妙的实现。您可以从几件好事中受益:首先,quotRem 应该使用欧几里得算法,该算法的较大参数是对数的。 (也许它更快,但我不相信有什么比欧几里得更快的常数因子。)此外,您可以像上次讨论的那样对列表进行操作,这样您就不必在处理时解决任何列表重击问题。 go - 当您返回解析列表时,列表已经完全构建完毕。如您所见,性能由此受益。

这段代码可能是 GHCi 中最慢的,因为 GHC 中使用 -O3 标志执行的许多优化都是为了使列表更快,而 GHCi 不会这样做。

Lessons:以正确的方式将 cons 放入列表中,注意可能减慢计算速度的中间严格性,并做一些跑腿工作来查看代码性能的细粒度统计数据。还要使用 -O3 标志进行编译:只要你不这样做,所有那些花费大量时间使 GHC 超快的人都会对你大眼瞪小眼。


Data:

我只是将所有四个函数粘贴到一个 .hs 文件中,然后根据需要进行更改以反映正在使用的函数。另外,我将限制提高到 5e6,因为在某些情况下,编译的代码在 1e6 上运行时间不到半秒,这可能会导致我们正在进行的测量出现粒度问题。

编译器选项:使用ghc --make -O3 [文件名].hs让 GHC 做一些优化。我们将使用以下命令将统计数据转储到标准错误数字+RTS -sstderr.

在数字 1 的情况下,转储到 -stderr 会得到如下所示的输出:

digits1 +RTS -sstderr
12000000
   2,885,827,628 bytes allocated in the heap
         446,080 bytes copied during GC
           3,224 bytes maximum residency (1 sample(s))
          12,100 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  5504 collections,     0 parallel,  0.06s,  0.03s elapsed
  Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    1.61s  (  1.66s elapsed)
  GC    time    0.06s  (  0.03s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    1.67s  (  1.69s elapsed)

  %GC time       3.7%  (1.5% elapsed)

  Alloc rate    1,795,998,050 bytes per MUT second

  Productivity  96.3% of total user, 95.2% of total elapsed

这里有三个关键统计数据:

  1. 使用的总内存:仅 1MB 意味着该版本非常节省空间。
  2. 总时间:1.61 秒现在没有任何意义,但我们将看看它与其他实现相比如何。
  3. 生产力:这只是 100% 减去垃圾收集;因为我们已经完成了 96.3%,这意味着我们没有创建大量留在内存中的对象。

好吧,让我们继续讨论版本 2。

digits2 +RTS -sstderr
12000000
   5,512,869,824 bytes allocated in the heap
       1,312,416 bytes copied during GC
           3,336 bytes maximum residency (1 sample(s))
          13,048 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0: 10515 collections,     0 parallel,  0.06s,  0.04s elapsed
  Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    3.20s  (  3.25s elapsed)
  GC    time    0.06s  (  0.04s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    3.26s  (  3.29s elapsed)

  %GC time       1.9%  (1.2% elapsed)

  Alloc rate    1,723,838,984 bytes per MUT second

  Productivity  98.1% of total user, 97.1% of total elapsed

好吧,我们看到了一个有趣的模式。

  1. 使用相同数量的内存。这意味着这是一个非常好的实现,尽管这可能意味着我们需要测试更高的样本输入以查看是否可以找到差异。
  2. 需要两倍的时间。我们稍后会回过头来猜测为什么会这样。
  3. 它实际上稍微更有效率,但考虑到 GC 并不是这两个程序的很大一部分,这对我们没有任何重大帮助。

版本3:

digits3 +RTS -sstderr
12000000
   3,231,154,752 bytes allocated in the heap
         832,724 bytes copied during GC
           3,292 bytes maximum residency (1 sample(s))
          12,100 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  6163 collections,     0 parallel,  0.02s,  0.02s elapsed
  Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    2.09s  (  2.08s elapsed)
  GC    time    0.02s  (  0.02s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    2.11s  (  2.10s elapsed)

  %GC time       0.7%  (1.0% elapsed)

  Alloc rate    1,545,701,615 bytes per MUT second

  Productivity  99.3% of total user, 99.3% of total elapsed

好吧,我们看到了一些奇怪的模式。

  1. 我们的总内存使用量仍为 1MB。所以我们没有遇到任何内存效率低下的问题,这很好。
  2. 我们还没有完全达到“digits1”,但我们已经很容易击败“digits2”了。
  3. GC 很少。 (请记住,任何超过 95% 的生产率都非常好,因此我们在这里并没有真正处理任何太重要的事情。)

最后是版本 4:

digits4 +RTS -sstderr
12000000
   1,347,856,636 bytes allocated in the heap
         270,692 bytes copied during GC
           3,180 bytes maximum residency (1 sample(s))
          12,100 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

  Generation 0:  2570 collections,     0 parallel,  0.00s,  0.01s elapsed
  Generation 1:     1 collections,     0 parallel,  0.00s,  0.00s elapsed

  INIT  time    0.00s  (  0.00s elapsed)
  MUT   time    1.09s  (  1.08s elapsed)
  GC    time    0.00s  (  0.01s elapsed)
  EXIT  time    0.00s  (  0.00s elapsed)
  Total time    1.09s  (  1.09s elapsed)

  %GC time       0.0%  (0.8% elapsed)

  Alloc rate    1,234,293,036 bytes per MUT second

  Productivity 100.0% of total user, 100.5% of total elapsed

哇扎!让我们来分解一下:

  1. 总共仍然是 1MB。这几乎肯定是这些实现的一个特性,因为它们在 5e5 和 5e7 的输入上保持在 1MB。如果你愿意的话,这是懒惰的证明。
  2. 我们削减了大约 32% 的原始时间,这非常令人印象深刻。
  3. 我怀疑这里的百分比反映了 -sstderr 监控的粒度,而不是对超光速粒子的任何计算。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么`(map digitalToInt) . show`这么快? 的相关文章

  • Laravel 上传前如何压缩图像?

    我正在制作一个图片库网站 用户可以在其中上传任何图像 它们将显示在前端 我需要在不影响图像质量的情况下压缩图像 以减小图像大小 以便页面加载速度不会影响那么大 我使用以下代码来上传图像 rules array file gt require
  • 我的用例可以合并到单个查询中而不影响性能吗?

    我主要着眼于改善表现查询的内容以及是否能够解决单一查询对于我的用例之一 解释如下 涉及到2张表 Table 1 EMPLOYEE column1 column2 email1 email2 column5 column6 Table 2 E
  • Android Drawable 绘图性能?

    在我看来 我有一个简单的 ARGB 可绘制对象 大约需要 2 毫秒才能绘制 但我可以在 0 5 毫秒内绘制与位图相同的文件 只是一些快速代码 我真的不能认为它是一个选项 优化可绘制对象的绘制速度的最佳方法是什么 这取决于可绘制的数量以及每个
  • 快速像素绘图库

    我的应用程序以每像素的方式生成 动画 因此我需要有效地绘制它们 我尝试过不同的策略 库 但结果并不令人满意 尤其是在更高分辨率的情况下 这是我尝试过的 SDL 好的 但是慢 OpenGL 像素操作效率低下 xlib 更好 但仍然太慢 svg
  • Android复杂布局线性和相对

    I have to implement a layout like shown in the diagram and I do not know the best combination to achieve the required de
  • IronPython 中批量求值表达式的性能

    在 C 4 0 应用程序中 我有一个具有相同长度的强类型 IList 的字典 一个基于动态强类型列的表 我希望用户根据将在所有行上聚合的可用列提供一个或多个 python 表达式 在静态上下文中它将是 IDictionary
  • 在 Haskell 中获取玫瑰树的根

    最近我开始学习 Haskell 并在以下练习中遇到困难 Write functions root Rose a gt a and children Rose a gt Rose a that return the value stored
  • 记录 Google Cloud SQL PostgreSQL 实例上的慢速查询

    我工作的公司使用 Google Cloud SQL 来管理生产中的 SQL 数据库 我们遇到了性能问题 我认为查看 监控高于特定阈值 例如 250 毫秒 的所有查询是一个好主意 除其他外 通过查看PostgreSQL 文档 https ww
  • Parsec.Expr 具有不同优先级的重复前缀

    Parsec Expr buildExpressionParser 的文档说 相同优先级的前缀和后缀运算符只能出现一次 即 如果 为前缀否定 则不允许使用 2 但是 我想解析这样的字符串 具体来说 考虑以下语法 sentence ident
  • 自定义 monad 的 MonadTransControl 实例

    的文档monad control提供有关如何创建实例的示例MonadTransControl using defaultLiftWith and defaultRestoreT 该示例适用于以下情况newtype newtype Count
  • 如何有效地扫描每次迭代交替的 2 位掩码

    给定 2 个位掩码 应交替访问 0 1 0 1 我尝试获得运行时高效的解决方案 但找不到比以下示例更好的方法 uint32 t mask 2 uint8 t mask index 0 uint32 t f tzcnt u32 mask ma
  • 如何在 Haskell Pipes 中将两个 Consumer 合并为一个?

    我使用Haskell流处理库pipes https hackage haskell org package pipes编写一个命令行工具 每个命令行操作都可以将结果输出到stdout并记录到stderr with pipes API I n
  • 每个 mmap/access/munmap 两次 TLB 未命中

    for int i 0 i lt 100000 i int page mmap NULL PAGE SIZE PROT READ PROT WRITE MAP ANONYMOUS MAP PRIVATE 1 0 page 0 0 munma
  • 性能 多次插入或多值单次插入

    从性能角度 时间和服务器负载 来看 最好是进行多个插入或单个插入多个值 我在 stackoverflow 上发现每次插入最多可以有 1000 个值集 我说的是两种情况 要插入大约 1000 3000 个值 有时我会在 mySQL 数据库中插
  • Blob 的簇生长

    考虑以下来自 Mathworks 的图像 我已经用标签标记了斑点 L num bwlabel I 如何迭代连接所有斑点 即从一个斑点开始 找到离它最近的一个 考虑最左边的两个斑点 可以从一个斑点的许多点绘制许多条线来连接到另一个斑点blob
  • 在哪里可以找到Python内置序列类型的时间和空间复杂度

    我一直无法找到此信息的来源 无法亲自查看 Python 源代码来确定这些对象是如何工作的 有谁知道我可以在网上找到这个吗 结帐时间复杂度 http wiki python org moin TimeComplexitypy dot org
  • perfmon 性能计数器是否基于与 xperf 使用的 ETW 事件“幕后”相同的东西?

    我最近开始熟悉 perfmon 和 xperf Perfmon 使用性能计数器 xperf 使用 ETW Windows 事件跟踪 Perfmon 具有提供数据的对象 而 xperf 使用 提供者 组 作为这个领域的新手 我想问是否有人可以
  • 网页优化:为什么组合文件速度更快?

    我读过 将所有 css 文件合并为一个大文件 或将所有脚本文件合并为一个脚本文件 可以减少 HTTP 请求的数量 从而加快下载速度 但我不明白这一点 我认为如果你有多个文件 最多有一个限制 我相信在现代浏览器上是 10 个 浏览器会并行下载
  • Mysql 更快的 INSERT

    好的 我有大约 175k 个 INSERT 语句 相当大的 INSERT 语句 例如 INSERT INTO gast ID Identiteitskaartnummer Naam Voornaam Adres Postcode Stad
  • 如何让 do 块提前返回?

    我正在尝试使用 Haskell 抓取网页并将结果编译到一个对象中 如果出于某种原因 我无法从页面获取所有项目 我想停止尝试处理页面并提前返回 例如 scrapePage String gt IO scrapePage url do doc

随机推荐