是什么使它成为在Web浏览器中打印1到1,000,000(以空格分隔)的最快的JavaScript?

时间:2020-03-06 14:45:48  来源:igfitidea点击:

我在这里阅读有关JavaScript的输出缓冲的信息,并试图弄清作者所说的最快将1到1,000,000打印到网页上的脚本。 (向下滚动到标题"中奖的一百万个数字脚本"。)在研究了一下之后,我有几个问题:

  • 与其他方法相比,是什么使该脚本如此高效?
  • 为什么缓冲可以加快速度?
  • 我们如何确定要使用的适当缓冲区大小?
  • 在座的人是否有任何技巧可以进一步优化此脚本?

(我意识到这可能是CS101,但我是那些自爆自学的黑客之一,我希望从集体中的智慧中受益。谢谢!)

解决方案

我敢打赌,打印100万个数字中最慢的是浏览器重新绘制页面,因此调用document.write()的次数越少越好。当然,这需要与大型字符串串联平衡(因为它们涉及分配和复制)。

通过实验可以确定合适的缓冲区大小。

在其他示例中,缓冲有助于沿着自然边界对齐。这里有些例子

  • 32位CPU可以更有效地传输32位。
  • TCP / IP数据包具有最大大小。
  • 文件I / O类具有内部缓冲区。
  • 像TIFF一样,图像也可以与数据一起存储在条带中。

与其他系统的自然边界保持一致通常可以带来性能上的好处。

考虑它的一种方法是考虑对document.write()的单个调用非常昂贵。但是,构建数组并将该数组连接到字符串中并非如此。因此,减少对document.write()的调用次数可以有效地减少写入数字所需的总体计算时间。

缓冲区是一种尝试将两个不同成本的工作捆绑在一起的方法。

对于小型阵列,计算和填充阵列的成本较低,对于大型阵列,计算和填充阵列的成本较高。
无论写大小如何,document.write的固定成本都很大,但对于较大的输入,其缩放比例小于o(n)。

因此,排队较大的字符串以进行写入或者缓冲可提高整体性能。

顺便说一下,这篇文章很不错。

与其他方法相比,是什么使该脚本如此高效?

作者对该算法进行了多种优化。所有这些都需要对如何使用底层机制(例如Javascript,CPU,寄存器,缓存,视频卡等)有相当深入的了解。我认为他正在进行2个关键的优化(其余只是锦上添花):

  • 缓冲输出
  • 使用整数数学而不是字符串操作

自我们明确询问缓冲以来,我将在稍后讨论缓冲。他使用的整数数学有两个性能优势:整数加法比字符串操作便宜每个操作,并且使用更少的内存。

我不知道JavaScript和Web浏览器如何在浏览器中处理整数到显示字形的转换,因此与字符串相比,将整数传递给document.write可能会带来一定的损失。但是,他正在执行(1,000,000 / 1000)document.write调用,而不是1,000,000 1,000个整数加法。这意味着,与将其发送到显示器相比,他所执行的形成消息的操作大约要多3个数量级。因此,将整数与字符串发送到document.write的代价必须超过3个数量级,才能抵消操作整数的性能优势。

为什么缓冲可以加快速度?

它起作用的具体细节取决于我们所使用的平台,硬件和实现。无论如何,这都是关于平衡算法和瓶颈诱导资源的。

例如,对于文件I / O,缓冲区很有用,因为它利用了旋转磁盘一次只能写入一定数量的事实。减少工作量,因为磁盘旋转时,它不会使用在主轴头下方经过的所有可用位。给它太多的空间,应用程序将不得不等待(或者进入睡眠状态),同时磁盘要花完写入时间,这可能会花费很多时间来准备下一条记录以进行写入!确定文件I / O理想缓冲区大小的一些关键因素包括:扇区大小,文件系统块大小,交织,磁头数,旋转速度和面密度。

就CPU而言,这就是保持流水线充满的原因。如果我们给CPU的工作量太少,它将在等待我们执行任务时花一些时间旋转无OP。如果给CPU过多的空间,则可能无法将请求分派到可能并行执行的其他资源,例如磁盘或者视频卡。这意味着稍后在CPU上将不得不等待这些返回而无所事事。在CPU中进行缓冲的主要因素是使我们需要的所有内容(对于CPU)尽可能地靠近FPU / ALU。在典型的体系结构中,这是(按递减的顺序):寄存器,L1高速缓存,L2高速缓存,L3高速缓存,RAM。

如果要在屏幕上写入一百万个数字,那就是用视频卡在屏幕上绘制多边形。这样想吧。假设对于每个添加的新数字,视频卡必须执行1亿次操作才能在屏幕上绘制多边形。在一个极端情况下,如果一次在页面上输入1个数字,然后将视频卡写出来,然后对1,000,000个数字执行此操作,则视频卡将必须执行10 ^ 14次操作100万亿次操作!在另一种极端情况下,如果我们将全部100万个号码全部一次发送到视频卡,则只需要进行1亿次操作。最佳点是中间的某个位置。如果一次执行一次,则CPU会执行一项工作,并等待很长时间,而GPU会更新显示内容。如果我们首先写入整个1​​M项目字符串,则CPU运转时GPU不会执行任何操作。

我们如何确定要使用的缓冲区大小?

除非我们使用特定算法(例如为Internet路由编写数据包路由)来针对一个非常特定且定义明确的平台,否则通常无法从数学上确定这一点。通常,我们可以凭经验找到它。猜一个值,尝试一下,记录结果,然后选择另一个。我们可以根据管理的瓶颈对从何处开始以及要研究的范围进行一些有根据的猜测。

在座的人是否有任何技巧可以进一步优化此脚本?

我不知道这是否行得通,而且我还没有对其进行测试,但是缓冲区大小通常为2的倍数,因为计算机的引脚固定为二进制,字长通常为2的倍数(但这并不总是案子!)。例如,64字节比60字节更可能是最佳选择,而1024比1000则更可能是最佳选择。这个问题的瓶颈之一是迄今为止,大多数浏览器(谷歌浏览器是我的第一个例外)知道),让javascript与其他网页呈现机制在同一线程内串行运行。这意味着javascript会做一些工作来填充缓冲区,然后等待很长时间,直到document.write调用返回。如果javascript以异步方式(例如在chrome中)作为单独的进程运行,则可能会大大提高速度。这当然是在攻击瓶颈的源头,而不是攻击使用瓶颈的算法,但有时这是最好的选择。

虽然没有我想要的那么简洁,但是希望这是一个很好的起点。缓冲是计算中各种性能问题的重要概念。很好地了解代码所使用的基本机制(硬件和软件)对于避免或者解决性能问题非常有用。

所以这个让我发疯了,因为我认为这真的不是最快的。所以这是我的实验,任何人都可以玩。也许我写错了什么,但似乎一次性完成所有操作而不使用缓冲区实际上是更快的操作。或者至少在我的实验中。

<html>
<head>
<script type="text/javascript">
    function printAllNumberBuffered(n, bufferSize)
    {
        var startTime = new Date();

        var oRuntime = document.getElementById("divRuntime");
        var oNumbers = document.getElementById("divNumbers");

        var i = 0;
        var currentNumber;
        var pass = 0;
        var numArray = new Array(bufferSize);

        for(currentNumber = 1; currentNumber <= n; currentNumber++)
        {
            numArray[i] = currentNumber;

            if(currentNumber % bufferSize == 0 && currentNumber > 0)
            {
                oNumbers.textContent += numArray.join(' ');
                i = 0;
            }
            else
            {
                i++;
            }
        }

        if(i > 0)
        {
            numArray.splice(i - 1, bufferSize - 1);
            oNumbers.textContent += numArray.join(' ');
        }

        var endTime = new Date();

        oRuntime.innerHTML += "<div>Number: " + n + " Buffer Size: " + bufferSize + " Runtime: " + (endTime - startTime) + "</div>";
    }

    function PrintNumbers()
    {
        var oNumbers = document.getElementById("divNumbers");
        var tbNumber = document.getElementById("tbNumber");
        var tbBufferSize = document.getElementById("tbBufferSize");

        var n = parseInt(tbNumber.value);
        var bufferSize = parseInt(tbBufferSize.value);

        oNumbers.textContent = "";

        printAllNumberBuffered(n, bufferSize);
    }
</script>
</head>
<body>
<table  border="1">
    <tr>
        <td colspan="2">
            <div>Number: <input id="tbNumber" type="text" />Buffer Size: <input id="tbBufferSize" type="text" /><input type="button" value="Run" onclick="PrintNumbers();" /></div>
        </td>
    </tr>
    <tr>
        <td style="vertical-align:top" width="30%">
            <div  id="divRuntime"></div>
        </td>
        <td width="90%">
            <div id="divNumbers"></div>
        </td>
    </tr>
</table>
</body>
</html>