国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

使用 PHP 處理十億行數據,如何極致提升處理速度?

譯文 精選
開發 前端
如果在閱讀這篇文章之前,你還不了解“十億行挑戰”( The One Billion Row Challenge,1brc ),我推薦你訪問 Gunnar Morling 的 1brc GitHub 代碼倉庫了解更多詳情。

譯者 | 劉汪洋

審校 | 重樓

如果在閱讀這篇文章之前,你還不了解“十億行挑戰”( The One Billion Row Challenge,1brc ),我推薦你訪問 Gunnar Morling 的 1brc GitHub 代碼倉庫了解更多詳情。

我有兩位同事已經參與這項挑戰并成功上榜,因此我也選擇加入。

雖然 PHP 的執行速度并不出名,但我正開發一個 PHP 分析器,因此我想親自測試一下 PHP的處理速度。

第一種嘗試:簡單直接的方法

我首先克隆了挑戰的代碼倉庫,并生成了一個包含十億行數據的文件measurements.txt。接下來,我開始嘗試第一個解決方案:

<?php

$stations = [];

$fp = fopen('measurements.txt', 'r');

while ($data = fgetcsv($fp, null, ';')) {
    if (!isset($stations[$data[0]])) {
        $stations[$data[0]] = [
            $data[1],
            $data[1],
            $data[1],
            1
        ];
    } else {
        $stations[$data[0]][3]++;
        $stations[$data[0]][2] += $data[1];
        if ($data[1] < $stations[$data[0]][0]) {
            $stations[$data[0]][0] = $data[1];
        }
        if ($data[1] > $stations[$data[0]][1]) {
            $stations[$data[0]][1] = $data[1];
        }
    }
}

ksort($stations);

echo '{';
foreach ($stations as $k => &$station) {
    $station[2] = $station[2] / $station[3];
    echo $k, '=', $station[0], '/', $station[2], '/', $station[1], ', ';
}
echo '}';

這段代碼邏輯簡單明了:打開文件并通過 fgetcsv()讀取數據。若之前未記錄過該站點,則創建一個新條目;否則,進行計數器增加、溫度累加,并檢查當前溫度是否刷新了最低或最高記錄,如是,則進行更新。

處理完所有數據后,我使用ksort()對數組$stations進行排序,并輸出每個站點的最低溫度、平均溫度(總溫度/記錄數)和最高溫度。

令我驚訝的是,在我的筆記本電腦上運行這段簡單腳本竟然耗時達到了25分鐘。

很明顯,我需要對這段代碼進行優化,并對其進行性能分析:

通過可視化的時間線,我們可以分析出腳本運行明顯受到 CPU 限制,腳本開始時的文件編譯時間可以忽略不計,且幾乎沒有垃圾收集事件發生。

火焰圖清晰地顯示出,fgetcsv()函數占據了約 46% 的 CPU 時間。

使用 fgets() 替代 fgetcsv()

為了提升性能,我決定用fgets()替換fgetcsv()函數來逐行讀取數據,并手動按;字符進行分割。

// ...
while ($data = fgets($fp, 999)) {
    $pos = strpos($data, ';');
    $city = substr($data, 0, $pos);
    $temp = substr($data, $pos + 1, -1);
// ...

同時,我還把代碼中的$data[0]重命名為$city,$data[1]重命名為$temp,以增強代碼的可讀性。

這個簡單的修改使得腳本運行時間大幅減少到 19 分鐘 49 秒,雖然時間仍然較長,但相比之前已經減少了 21%。

通過火焰圖的比較,可以看到在替換后 CPU 的時間利用率發生了變化,詳細的根幀分析也揭示了具體的性能瓶頸位置:

在腳本的第 18 行和第 23 行花費了大約 38% 的CPU時間。

18 | $stations[$city][3]++;
   | // ...
23 | if ($temp > $stations[$city][1]) {

第 18 行是數組$stations的首次訪問和增量操作,而第 23 行進行了一次看似不那么耗時的比較操作。盡管如此,進一步優化有助于揭示這些操作中潛在的性能開銷。

盡可能使用引用

為了提高性能,我決定在處理數組時使用引用,以避免每次訪問數組時都對$stations數組中的鍵進行搜索。這相當于為數組中的"當前"站點設置了一個緩存。

代碼如下:

$station = &$stations[$city];
$station[3]++;
$station[2] += $temp;
// 替代原有的
$stations[$city][3]++;
$stations[$city][2] += $temp;

這一改變實際上大大減少了執行時間,將其縮短到 17 分鐘 48 秒,進一步減少了 **10% **的運行時間。

條件判斷優化

在審查代碼的過程中,我注意到了以下片段:

if ($temp < $station[0]) {
    $station[0] = $temp;
} elseif ($temp > $station[1]) {
    $station[1] = $temp;
}

考慮到一個溫度值如果低于最小值,則不可能同時高于最大值,因此我使用elseif來優化條件判斷,這可能會節省一些 CPU 周期。

需要指出的是,由于我不知道measurements.txt中溫度值的排列順序,根據這個順序,首先檢查最小值還是最大值可能會有所不同。

這次優化將時間進一步縮短到 17 分鐘 30 秒,節省了大約 2% 的時間,雖然這個提升并不是非常顯著。

執行類型轉換

PHP是一種動態類型語言,我在編程初期非常欣賞它這一特點,因為它簡化了許多問題。然而,另一方面,明確變量類型能幫助解釋引擎更高效地執行代碼。

$temp = (float)substr($data, $pos + 1, -1);

令人驚訝的是,這個簡單的類型轉換把腳本執行時間縮短至 13 分鐘 32 秒,性能提升達到了驚人的 **21% **!

18 | $station = &$stations[$city];
   | // ...
23 | } elseif ($temp > $station[1]) {

在優化后,第 18 行顯示數組訪問的 CPU 時間消耗從 11% 減少,這是因為減少了在 PHP 的哈希映射(關聯數組的底層數據結構)中搜索鍵的次數。

第 23 行的 CPU 時間從約 32% 減少到約 15%。這是因為避免了類型轉換的開銷。在優化之前,$temp、$station[0]和$station[1]是字符串類型,因此 PHP 在每次比較時必須將它們轉換為浮點數。

引入 JIT

在優化過程中,我還嘗試啟用了 PHP 的 JIT(即時編譯器),它是 OPCache 的一部分。默認情況下,OPCache 在 CLI(命令行界面)模式下被禁用,因此需通過將opcache.enable_cli 設置為 on來啟用。此外,雖然JIT默認為開啟狀態,但由于緩沖區大小默認設置為0,實際上處于禁用狀態。通過將opcache.jit-buffer-size設置為10M,我有效地啟用了 JIT。

啟用 JIT 后,腳本執行時間驚人地縮減至 7 分鐘 19 秒,速度提升了 45.9%。

進一步優化

通過這系列優化,我將腳本的執行時間從最初的 25 分鐘大幅降低到了約 7 分鐘。在這個過程中,我注意到使用fgets()讀取一個 13GB 的文件時,竟然分配了大約 56GiB 每分鐘的 RAM,這顯然是不合理的。經過調查,我發現省略fgets()的長度參數可以大量減少內存分配:

while ($data = fgets($fp)) {
// 替代之前的
while ($data = fgets($fp, 999)) {

這個簡單變化雖然只使性能提高了約 1%,但將內存分配從每分鐘 56GiB 降至每分鐘 6GiB,顯著減少了內存占用。這一改進雖然對執行時間影響不大,但減少內存消耗對于大規模數據處理仍然是一個重要的優化方向。

以上優化展示了在 PHP 性能調優中考慮各種因素的重要性,包括代碼邏輯優化、類型明確、JIT編譯以及內存管理等,共同作用下可以顯著提升應用性能。

還能更快嗎?

到目前為止,我使用的單線程方法,與許多PHP程序默認的單線程方式相符,但通過使用parallel 擴展,PHP 實際上能在用戶空間內實現多線程操作。

性能分析明確指出,在 PHP 中進行數據讀取成為了性能瓶頸。雖然從 fgetcsv() 切換到 fgets() 并手動進行字符串分割有所改進,但這種方式仍舊相對耗時。因此,我們考慮采用多線程的方式來并行地讀取和處理數據,并在之后將各個工作線程的中間結果合并起來。

<?php

$file = 'measurements.txt';

$threads_cnt = 16;

/**
 * 計算并返回每個線程應處理的文件塊的起始和結束位置。
 * 這些位置將基于 \n 字符進行對齊,因為我們使用 `fgets()` 進行讀取,
 * 它會讀取直到遇到 \n 字符為止。
 *
 * @return array<int, array{0: int, 1: int}>
 */
function get_file_chunks(string $file, int $cpu_count): array {
    $size = filesize($file);

    if ($cpu_count == 1) {
        $chunk_size = $size;
    } else {
        $chunk_size = (int) ($size / $cpu_count);
    }

    $fp = fopen($file, 'rb');

    $chunks = [];
    $chunk_start = 0;
    while ($chunk_start < $size) {
        $chunk_end = min($size, $chunk_start + $chunk_size);

        if ($chunk_end < $size) {
            fseek($fp, $chunk_end);
            fgets($fp); // 將文件指針移動到下一個 \n 字符
            $chunk_end = ftell($fp);
        }

        $chunks[] = [
            $chunk_start,
            $chunk_end
        ];

        $chunk_start = $chunk_end;
    }

    fclose($fp);
    return $chunks;
}

/**
 * 該函數負責打開指定的 `$file` 文件,并從 `$chunk_start` 開始讀取處理數據,
 * 直到達到 `$chunk_end`。
 *
 * 返回的結果數組以城市名作為鍵,其值為一個數組,包含最低溫度(鍵 0)、最高溫度(鍵 1)、
 * 溫度總和(鍵 2)及溫度計數(鍵 3)。
 *
 * @return array<string, array{0: float, 1: float, 2: float, 3: int}>
 */
$process_chunk = function (string $file, int $chunk_start, int $chunk_end): array {
    $stations = [];
    $fp = fopen($file, 'rb');
    fseek($fp, $chunk_start);
    while ($data = fgets($fp)) {
        $chunk_start += strlen($data);
        if ($chunk_start > $chunk_end) {
            break;
        }
        $pos2 = strpos($data, ';');
        $city = substr($data, 0, $pos2);
        $temp = (float)substr($data, $pos2 + 1, -1);
        if (isset($stations[$city])) {
            $station = &$stations[$city];
            $station[3]++;
            $station[2] += $temp;
            if ($temp < $station[0]) {
                $station[0] = $temp;
            } elseif ($temp > $station[1]) {
                $station[1] = $temp;
            }
        } else {
            $stations[$city] = [
                $temp,
                $temp,
                $temp,
                1
            ];
        }
    }
    return $stations;
};

$chunks = get_file_chunks($file, $threads_cnt);

$futures = [];

for ($i = 0; $i < $threads_cnt; $i++) {
    $runtime = new \parallel\Runtime();
    $futures[$i] = $runtime->run(
        $process_chunk,
        [
            $file,
            $chunks[$i][0],
            $chunks[$i][1]
        ]
    );
}

$results = [];

for ($i = 0; $i < $threads_cnt; $i++) {
    // 等待線程結果,主線程在此處阻塞直至獲取結果
    $chunk_result = $futures[$i]->value();
    foreach ($chunk_result as $city => $measurement) {
        if (isset($results[$city])) {
            $result = &$results[$city];
            $result[2] += $measurement[2];
            $result[3] += $measurement[3];
            if ($measurement[0] < $result[0]) {
                $result[0] = $measurement[0];
            }
            if ($measurement[1] > $result[1]) {
                $result[1] = $measurement[1];
            }
        } else {
            $results[$city] = $measurement;
        }
    }
}

ksort($results);

echo '{', PHP_EOL;
foreach ($results as $k => &$station) {
    echo "\t", $k, '=', $station[0], '/', ($station[2] / $station[3]), '/', $station[1], ',', PHP_EOL;
}
echo '}', PHP_EOL;

該段代碼主要執行以下操作:首先,它掃描文件并將其分割成以 \n 為界的塊(利用 fgets() 進行讀取)。準備好這些塊后,我啟動了 $threads_cnt 個工作線程,它們分別打開相同的文件并跳轉到分配給它們的塊的起始位置,繼續讀取并處理數據直到塊結束,返回中間結果。最后,在主線程中合并、排序并輸出這些結果。

利用多線程處理,這個過程只需:??** 1 分 35 秒** ??

這樣就結束了?

當然沒有。想要進一步優化至少還需考慮的兩個重要方面:

  1. 我在搭載 Apple Silicon 的 MacOS 系統上運行這段代碼時發現,當在 PHP 的 ZTS(Zend Thread Safety)版本中啟用 JIT(Just-In-Time 編譯)功能時,程序會遇到崩潰問題。因此,我得到的 1 分 35 秒的執行時間是在未啟用 JIT 的情況下測得的。如果能夠啟用 JIT,執行速度可能會更快。
  2. 我后來意識到,我運行的 PHP 版本是使用 CFLAGS="-g -O0 ..." 編譯的,主要是因為日常工作需要。

我原本應該從一開始就檢查編譯標志。因此,我后來使用 CFLAGS="-Os ..." 重新編譯了 PHP 8.3,最終在使用 16 個線程的情況下得到了:

?? 27.7 秒 ??

這個成績無法與原始挑戰榜上的成績直接比較,因為我是在完全不同的硬件環境下運行的代碼。

以下是使用 10 個線程時的時間線視圖:

圖示中底部的線程代表主線程,在等待工作線程返回結果。工作線程一旦返回中間結果,我們便可見到主線程開始對所有結果進行合并和排序。從中可以明顯看出,主線程并不是性能瓶頸。如果想進一步優化性能,應該集中關注工作線程的效率。

我從這個過程中學到了什么?

  • 每一層抽象不僅僅是為了易用性或集成而設計,同時也以犧牲 CPU 周期或內存為代價。fgetcsv() 雖然使用簡便,卻隱藏了很多細節,這是有成本的。即便是使用 fgets(),它雖然簡化了數據讀取,但也同樣掩蓋了某些操作。
  • 在代碼中添加類型聲明有助于語言優化執行過程,或避免類型混用所需的 CPU 周期,盡管這些開銷你可能看不見。
  • JIT 在處理 CPU 密集型任務時表現出色。

雖然這不是大多數 PHP 應用的常態,但得益于并行化(通過 [ext-parallel] 實現),我們成功地大幅縮短了處理時間。

結論

我希望你能從這篇文章中感受到我寫作時的樂趣。如果你有興趣進一步優化這段代碼,歡迎前來挑戰,并通過評論告訴我你的成果。

譯者介紹

劉汪洋,51CTO社區編輯,昵稱:明明如月,一個擁有 5 年開發經驗的某大廠高級 Java 工程師,擁有多個主流技術博客平臺博客專家稱號。

原文標題:Processing One Billion Rows in PHP!,作者:Florian Engelhardt

鏈接:https://dev.to/realflowcontrol/processing-one-billion-rows-in-php-3eg0

責任編輯:華軒 來源: 51CTO
相關推薦

2023-09-27 15:34:48

數據編程

2009-10-14 14:27:44

DataPlatforInformatica數據平臺

2023-05-05 19:29:41

2021-07-17 22:41:53

Python數據技術

2022-01-26 09:00:00

數據庫SnowparkSQL

2018-01-24 11:49:34

2023-11-29 10:16:24

ScrapyPython

2009-12-10 16:40:04

PHP處理分頁

2019-12-23 08:57:50

Python代碼單線程

2009-12-08 16:19:21

PHP函數pack

2009-12-02 09:49:43

PHP Ajax亂碼

2009-12-07 17:05:36

PHP函數imaget

2013-11-11 09:08:34

40 GbE40G以太網

2024-10-30 10:00:00

Python函數

2020-12-03 15:54:15

軟件開發工具

2010-02-23 15:25:10

CentOS Apac

2016-05-12 16:39:57

IT運維網絡

2012-08-09 09:11:32

PHP超時

2009-11-25 09:56:06

PHP數組處理函數

2010-09-08 13:35:59

點贊
收藏

51CTO技術棧公眾號

国产精品久久久久久久午夜| 日韩欧美国产免费| 一本一道久久a久久| 91精选在线观看| 久草电影在线| 亚洲丝袜美腿综合| 黄色免费网址大全| 久久久久久9999| 国产人妻777人伦精品hd| 蜜臀av性久久久久蜜臀aⅴ流畅 | 天堂中文字幕| 国产日产欧美一区二区三区| 国产极品粉嫩福利姬萌白酱| 粉嫩久久99精品久久久久久夜| 在线观看一区欧美| 国产伦精品一区二区三区免费迷| 中文字幕剧情在线观看一区| 蜜桃91丨九色丨蝌蚪91桃色| 亚洲美女自拍偷拍| 美女精品一区| 国产日韩精品在线观看| 国产一线二线三线女| www.99色.com| 日韩一区二区三区免费看| 网址你懂得在线观看| 亚洲人被黑人高潮完整版| 黄网视频在线观看| 一区二区三区四区视频精品免费| 传媒av在线| 精品中文字幕一区二区三区| 在线日韩欧美视频| 亚洲精品v亚洲精品v日韩精品| 97在线看福利| 久久精品久久久| 黄色99视频| 另类中文字幕网| 一女被多男玩喷潮视频| 久久久噜噜噜久噜久久综合| 国产xxxxx| 欧美三级电影网| 欧美大胆a人体大胆做受| 色吧影院999| aaa国产精品视频| 成人a级免费视频| 裸体一区二区| 日韩在线综合网| 亚洲国产另类精品专区| 都市激情一区| 亚洲欧洲视频在线| 亚洲激情播播| 欧美日韩在线不卡一区| 99麻豆久久久国产精品免费| 传媒视频在线| 日韩国产一区三区| 色婷婷久久久| 欧美视频1区| 99re6这里只有精品视频在线观看| 中文字幕2018| 日韩精品视频在线观看免费| 伊人精品综合| 久久婷婷开心| 久久久激情视频| 成人在线二区| 欧美人与性动交| 激情综合久久| 日本成人黄色网| 欧美色图国产精品| 精品一区二区不卡| 国产亚洲欧美日韩日本| 91在线视频| 欧美高清视频在线播放| 亚洲大黄网站| 黄色三级视频在线| 日韩一级精品视频在线观看| 欧美亚洲大陆| 中文字幕色一区二区| 亚洲一区二区成人在线观看| 亚洲欧洲日本韩国| 国产日韩在线一区| 成人av电影在线播放| 国产黄色片在线播放| 欧美高清第一页| 美国三级日本三级久久99| 校园春色影音先锋| 色七七影院综合| 先锋影音久久久| 淫视频在线观看| 一二美女精品欧洲| 一区二区三区四区五区精品视频| 美女网站免费观看视频| 欧美大片日本大片免费观看| 久久国产影院| 日韩av手机版| 亚洲人成在线观看| 在线播放精品| 一级一片免费视频| 色综合天天综合网国产成人网| 久久亚洲综合| 青青色在线视频| 欧美一级电影久久| 成人免费黄色大片| 24小时免费看片在线观看| 超碰在线观看97| 一区二区三区毛片| 136福利精品导航| 三上悠亚久久精品| 亚洲国内精品视频| 亚洲免费在线| a√资源在线| 成人乱色短篇合集| 一区二区理论电影在线观看| 欧美.com| 黄在线观看网站| 中文字幕亚洲字幕| 国产在线精品一区二区三区不卡 | 国产美女被遭强高潮免费网站| 亚洲情综合五月天| 日韩黄色免费电影| 最新国产在线观看| 亚洲最大成人网色| 欧美日韩精品在线| 91中文字幕精品永久在线| 久久综合色播| 国产精品99久久久久久久久久久久| 国产亚洲精品资源在线26u| 亚洲成人精品综合在线| 国产伦精品一区二区三区四区视频_ | 香蕉久久一区| 精品视频在线观看一区| 精品一区精品二区| 国产麻豆精品视频| 综合日韩av| 日韩国产小视频| 一区二区三区在线播放欧美| 国产+成+人+亚洲欧洲自线| 中文字幕影音在线| www污在线观看| 不卡av在线网站| 中文字幕精品三区| 亚洲视频分类| 最新中文在线视频| 成人av免费在线看| 91精品视频网| 美女网站在线免费欧美精品| 在线观看特色大片免费视频| 免费超爽大片黄| 欧美肥老妇视频| 综合电影一区二区三区 | 国产在线视频福利| 久久久亚洲综合网站| 精品少妇一区二区三区免费观看| 青青草成人在线观看| 国模套图日韩精品一区二区| 欧美性大战久久久久xxx| 久久久人成影片一区二区三区观看 | 国产精品视频yy9299一区| 久久久亚洲欧洲日产| 黄动漫在线免费观看| 国产精品裸体一区二区三区| 欧美精品一区二区在线观看| 不卡一区二区三区四区| 性欧美lx╳lx╳| 调教视频免费在线观看| 国产911在线观看| 97婷婷大伊香蕉精品视频| 亚洲动漫第一页| 日本欧美一区二区| 日韩三级精品| 免费在线一级视频| 这里只有精品66| 欧美激情精品久久久久久蜜臀| 一区二区成人在线| 麻豆精品网站| 精品中文字幕一区二区三区| 一级片在线观看| 26uuu成人| 日本久久久久亚洲中字幕| 欧美三级电影在线观看| 丰满少妇久久久久久久| 波多野结衣一区| 蜜桃视频动漫在线播放| 可以看美女隐私的网站| 欧美日韩一区在线播放| 欧美激情视频在线免费观看 欧美视频免费一 | 亚洲男人在线| 亚洲福利二区| 9191国产视频| 成人自拍性视频| 一个人www欧美| 午夜精品福利一区二区三区av| 九九国产精品视频| 性人久久久久| av小说在线播放| 日本搞黄视频| 国产91沈先生在线播放| aa成人免费视频| 欧美日韩成人在线播放| 精品免费视频一区二区| 午夜精品久久久久久久蜜桃app| 成人黄色av电影|