簡體   English   中英

為什么從new []添加兩個std :: vectors比原始數組慢?

[英]Why is adding two std::vectors slower than raw arrays from new[]?

我正在尋找OpenMP,部分原因是我的程序需要添加非常大的向量(數百萬個元素)。 但是,如果我使用std :: vector或raw數組,我會看到相當大的差異。 我無法解釋。 我堅持認為差異只在於循環,而不是當然的初始化。

我所指的時間差異,只是添加的時間,特別是不考慮矢量,數組等之間的任何初始化差異。我實際上只談論總和部分。 在編譯時不知道向量的大小。 我在Ubuntu 16.04上使用g++ 5.x.

編輯:我測試了什么@Shadow說,它讓我思考,是否有一些優化的事情? 如果我使用-O2編譯,那么,使用初始化的原始數組,我會回到使用線程數進行循環擴展。 但是使用-O3-funroll-loops ,就好像編譯器會提前啟動並在看到編譯指示之前進行優化。

我想出了以下簡單測試:

#define SIZE 10000000
#define TRIES 200
int main(){

    std::vector<double> a,b,c;
    a.resize(SIZE);
    b.resize(SIZE);
    c.resize(SIZE);

    double start = omp_get_wtime();
    unsigned long int i,t;
    #pragma omp parallel shared(a,b,c) private(i,t)
    {
    for( t = 0; t< TRIES; t++){
       #pragma omp for
       for( i = 0; i< SIZE; i++){
        c[i] = a[i] + b[i];
       }
    }
    }

    std::cout << "finished in " << omp_get_wtime() - start << std::endl;

    return 0;

}

我編譯

   g++ -O3 -fopenmp  -std=c++11 main.cpp

獲得一個線程

>time ./a.out 
 finished in 2.5638
 ./a.out  2.58s user 0.04s system 99% cpu 2.619 total.

對於兩個線程,循環需要1.2s,總共1.23。

現在,如果我使用原始數組:

 int main(){
    double *a, *b, *c;
    a = new double[SIZE];
    b = new double[SIZE];
    c = new double[SIZE];
    double start = omp_get_wtime();
    unsigned long int i,t;
    #pragma omp parallel shared(a,b,c) private(i,t)
    {
       for( t = 0; t< TRIES; t++)
       {
          #pragma omp for
          for( i = 0; i< SIZE; i++)
          {
             c[i] = a[i] + b[i];
          }
       }
    }

    std::cout << "finished in " << omp_get_wtime() - start << std::endl;

    delete[] a;
    delete[] b;
    delete[] c;

    return 0;
}

我得到(1線程):

>time ./a.out
 finished in 1.92901 
  ./a.out  1.92s user 0.01s system 99% cpu 1.939 total   

std::vector慢了33%!

對於兩個線程:

>time ./a.out 
finished in 1.20061                                                              
./a.out  2.39s user 0.02s system 198% cpu 1.208 total   

作為比較,使用Eigen或Armadillo進行完全相同的操作(使用c = a + b帶矢量對象的重載),我得到總實時~2.8s。 它們不是用於向量添加的多線程。

現在,我認為std::vector幾乎沒有開銷? 這里發生了什么? 我想使用漂亮的標准庫對象。

在這樣一個簡單的例子中,我找不到任何參考。

有意義的基准測試很難

來自Xirema的答案已經詳細列出了代碼中的差異 std::vector::reserve 將數據初始化為零,而new double[size]則不會。 請注意,您可以使用new double[size]()來強制初始化。

但是,您的測量不包括初始化,並且重復次數非常多,即使在Xirema的示例中,循環成本也應該超過小型初始化。 那么為什么循環中的相同指令需要更多時間,因為數據已初始化?

最小的例子

讓我們用一個動態確定內存是否初始化的代碼來挖掘它的核心(基於Xirema,但只對循環本身進行定時)。

#include <vector>
#include <chrono>
#include <iostream>
#include <memory>
#include <iomanip>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <unistd.h>

constexpr size_t size = 10'000'000;

auto time_pointer(size_t reps, bool initialize, double init_value) {
    double * a = new double[size];
    double * b = new double[size];
    double * c = new double[size];

    if (initialize) {
        for (size_t i = 0; i < size; i++) {
            a[i] = b[i] = c[i] = init_value;
        }
    }

    auto start = std::chrono::steady_clock::now();

    for (size_t t = 0; t < reps; t++) {
        for (size_t i = 0; i < size; i++) {
            c[i] = a[i] + b[i];
        }
    }

    auto end = std::chrono::steady_clock::now();

    delete[] a;
    delete[] b;
    delete[] c;

    return end - start;
}

int main(int argc, char* argv[]) {
    bool initialize = (argc == 3);
    double init_value = 0;
    if (initialize) {
        init_value = std::stod(argv[2]);
    }
    auto reps = std::stoll(argv[1]);
    std::cout << "pid: " << getpid() << "\n";
    auto t = time_pointer(reps, initialize, init_value);
    std::cout << std::setw(12) << std::chrono::duration_cast<std::chrono::milliseconds>(t).count() << "ms" << std::endl;
    return 0;
}

結果是一致的:

./a.out 50 # no initialization
657ms
./a.out 50 0. # with initialization
1005ms

乍一看性能指標

使用優秀的Linux perf工具:

$ perf stat -e LLC-loads -e dTLB-misses ./a.out 50  
pid: 12481
         626ms

 Performance counter stats for './a.out 50':

       101.589.231      LLC-loads                                                   
           105.415      dTLB-misses                                                 

       0,629369979 seconds time elapsed

$ perf stat -e LLC-loads -e dTLB-misses ./a.out 50 0.
pid: 12499
        1008ms

 Performance counter stats for './a.out 50 0.':

       145.218.903      LLC-loads                                                   
         1.889.286      dTLB-misses                                                 

       1,096923077 seconds time elapsed

隨着重復次數的增加,線性縮放也告訴我們,差異來自循環內部。 但是為什么初始化內存會導致更多的最后一級緩存加載和數據TLB未命中?

記憶很復雜

要理解這一點,我們需要了解內存的分配方式。 僅僅因為malloc / new返回一些指向虛擬內存的指針,並不意味着它背后有物理內存。 虛擬內存可以位於不受物理內存支持的頁面中 - 物理內存僅在第一頁故障時分配。 現在這里是page-types (來自linux/tools/vm - 以及我們顯示為輸出的pid派上用場。在長期執行我們的小基准測試期間查看頁面統計信息:

隨着初始化

                 flags  page-count       MB  symbolic-flags         long-symbolic-flags
    0x0000000000000804           1        0  __R________M______________________________ referenced,mmap
    0x000000000004082c         392        1  __RU_l_____M______u_______________________ referenced,uptodate,lru,mmap,unevictable
    0x000000000000086c         335        1  __RU_lA____M______________________________ referenced,uptodate,lru,active,mmap
    0x0000000000401800       56721      221  ___________Ma_________t___________________ mmap,anonymous,thp
    0x0000000000005868        1807        7  ___U_lA____Ma_b___________________________ uptodate,lru,active,mmap,anonymous,swapbacked
    0x0000000000405868         111        0  ___U_lA____Ma_b_______t___________________ uptodate,lru,active,mmap,anonymous,swapbacked,thp
    0x000000000000586c           1        0  __RU_lA____Ma_b___________________________ referenced,uptodate,lru,active,mmap,anonymous,swapbacked
                 total       59368      231

大多數虛擬內存都在普通的mmap,anonymous區域 - 映射到物理地址的東西。

沒有初始化

             flags  page-count       MB  symbolic-flags         long-symbolic-flags
0x0000000001000000        1174        4  ________________________z_________________ zero_page
0x0000000001400000       37888      148  ______________________t_z_________________ thp,zero_page
0x0000000000000800           1        0  ___________M______________________________ mmap
0x000000000004082c         388        1  __RU_l_____M______u_______________________ referenced,uptodate,lru,mmap,unevictable
0x000000000000086c         347        1  __RU_lA____M______________________________ referenced,uptodate,lru,active,mmap
0x0000000000401800       18907       73  ___________Ma_________t___________________ mmap,anonymous,thp
0x0000000000005868         633        2  ___U_lA____Ma_b___________________________ uptodate,lru,active,mmap,anonymous,swapbacked
0x0000000000405868          37        0  ___U_lA____Ma_b_______t___________________ uptodate,lru,active,mmap,anonymous,swapbacked,thp
0x000000000000586c           1        0  __RU_lA____Ma_b___________________________ referenced,uptodate,lru,active,mmap,anonymous,swapbacked
             total       59376      231

現在,只有1/3的內存由專用的物理內存支持,2/3的內存映射到零頁 ab后面的數據全部由一個填充零的只讀4kiB頁面支持。 c (和另一個測試中的ab )已被寫入,因此必須擁有自己的內存。

0!= 0

現在它可能看起來很奇怪:這里的一切都是零1 - 為什么它變成零怎么回事? 無論你是memset(0)a[i] = 0. ,還是std::vector::reserve - 一切都會導致對內存的顯式寫入,因此如果你在零頁面上執行它就會出現頁面錯誤。 我不認為你可以/應該阻止那時的物理頁面分配。 你可以為memset / reserve做的唯一事情就是使用calloc顯式請求零內存,這可能是由zero_page支持的,但我懷疑它已經完成(或者很有意義)。 請記住,對於new double[size]; 或者malloc 不能保證你得到什么樣的內存,但這包括零內存的可能性。

1 :請記住,double 0.0將所有位設置為零。

最后,性能差異實際上只來自循環 ,但是由初始化引起 std::vector 沒有循環開銷 在基准代碼中,原始數組只會受益於未初始化數據的異常情況的優化。

我有一個很好的假設。

我編寫了三個版本的代碼:一個使用raw double * ,一個使用std::unique_ptr<double[]>對象,另一個使用std::vector<double> ,並比較了每個版本的運行時間編碼。 出於我的目的,我使用了單線程版本的代碼來嘗試簡化案例。

總代碼

#include<vector>
#include<chrono>
#include<iostream>
#include<memory>
#include<iomanip>

constexpr size_t size = 10'000'000;
constexpr size_t reps = 50;

auto time_vector() {
    auto start = std::chrono::steady_clock::now();
    {
        std::vector<double> a(size);
        std::vector<double> b(size);
        std::vector<double> c(size);

        for (size_t t = 0; t < reps; t++) {
            for (size_t i = 0; i < size; i++) {
                c[i] = a[i] + b[i];
            }
        }
    }
    auto end = std::chrono::steady_clock::now();
    return end - start;
}

auto time_pointer() {
    auto start = std::chrono::steady_clock::now();
    {
        double * a = new double[size];
        double * b = new double[size];
        double * c = new double[size];

        for (size_t t = 0; t < reps; t++) {
            for (size_t i = 0; i < size; i++) {
                c[i] = a[i] + b[i];
            }
        }

        delete[] a;
        delete[] b;
        delete[] c;
    }
    auto end = std::chrono::steady_clock::now();
    return end - start;
}

auto time_unique_ptr() {
    auto start = std::chrono::steady_clock::now();
    {
        std::unique_ptr<double[]> a = std::make_unique<double[]>(size);
        std::unique_ptr<double[]> b = std::make_unique<double[]>(size);
        std::unique_ptr<double[]> c = std::make_unique<double[]>(size);

        for (size_t t = 0; t < reps; t++) {
            for (size_t i = 0; i < size; i++) {
                c[i] = a[i] + b[i];
            }
        }
    }
    auto end = std::chrono::steady_clock::now();
    return end - start;
}

int main() {
    std::cout << "Vector took         " << std::setw(12) << time_vector().count() << "ns" << std::endl;
    std::cout << "Pointer took        " << std::setw(12) << time_pointer().count() << "ns" << std::endl;
    std::cout << "Unique Pointer took " << std::setw(12) << time_unique_ptr().count() << "ns" << std::endl;
    return 0;
}

檢測結果:

Vector took           1442575273ns //Note: the first one executed, regardless of 
    //which function it is, is always slower than expected. I'll talk about that later.
Pointer took           542265103ns
Unique Pointer took   1280087558ns

因此,所有STL對象都明顯慢於原始版本。 為什么會這樣?

我們去大會吧! (使用Godbolt.com編譯,使用GCC 8.x的快照版本)

我們可以從一開始就觀察到一些事情。 首先, std::unique_ptrstd::vector代碼生成幾乎相同的匯編代碼。 std::unique_ptr<double[]>換掉newdelete new[]delete[] 由於它們的運行時間在誤差范圍內,我們將專注於std::unique_ptr<double[]>版本並將其與double *進行比較。

.L5.L22 ,代碼似乎完全相同。 唯一的主要區別是在double *版本中進行delete[]調用之前的額外指針運算,以及.L34std::unique_ptr<double[]>版本)末尾的一些額外堆棧清理代碼, double *版本不存在。 這些似乎都不會對代碼速度產生強烈影響,因此我們暫時忽略它們。

相同的代碼似乎是直接負責循環的代碼。 您會注意到不同的代碼(我將暫時得到)不包含任何跳轉語句,這些語句是循環的組成部分。

因此,所有主要差異似乎都與所討論對象的初始分配有關。 這是在.L32 std::unique_ptr<double[]>版本的time_unique_ptr():.L32之間,以及在double *版本的time_pointer():.L22之間。

那有什么區別? 好吧,他們幾乎做同樣的事情。 除了在std::unique_ptr<double[]>版本中顯示的幾行代碼,這些代碼沒有顯示在double *版本中:

std::unique_ptr<double[]>

mov     edi, 80000000
mov     r12, rax
call    operator new[](unsigned long)
mov     edx, 80000000
mov     rdi, rax
xor     esi, esi //Sets register to 0, which is probably used in...
mov     rbx, rax
call    memset //!!!
mov     edi, 80000000
call    operator new[](unsigned long)
mov     rdi, rax
mov     edx, 80000000
xor     esi, esi //Sets register to 0, which is probably used in...
mov     rbp, rax
call    memset //!!!
mov     edi, 80000000
call    operator new[](unsigned long)
mov     r14, rbx
xor     esi, esi //Sets register to 0, which is probably used in...
mov     rdi, rax
shr     r14, 3
mov     edx, 80000000
mov     r13d, 10000000
and     r14d, 1
call    memset //!!!

double *

mov     edi, 80000000
mov     rbp, rax
call    operator new[](unsigned long)
mov     rbx, rax
mov     edi, 80000000
mov     r14, rbx
shr     r14, 3
call    operator new[](unsigned long)
and     r14d, 1
mov     edi, 80000000
mov     r12, rax
sub     r13, r14
call    operator new[](unsigned long)

那你好看! memset一些意外調用不屬於double *代碼! 很明顯std::vector<T>std::unique_ptr<T[]>被約定為“初始化”他們分配的內存,而double *沒有這樣的合同。

因此,這基本上是一種非常非常圓潤的方式來驗證Shadow觀察到的內容:當你沒有嘗試“零填充”數組時,編譯器將

  • 什么都不做double * (節省寶貴的CPU周期),和
  • 在沒有提示std::vector<double>std::unique_ptr<double[]>情況下進行初始化(花費時間初始化所有內容)。

但是,當加填零,編譯器識別出它是即將“重演”,優化了第二個零填補std::vector<double>std::unique_ptr<double[]>導致代碼沒有改變)並將其添加到double *版本,使其與其他兩個版本相同。 您可以通過將我進行了以下更改的程序集新版本double *版本進行比較來確認:

double * a = new double[size];
for(size_t i = 0; i < size; i++) a[i] = 0;
double * b = new double[size];
for(size_t i = 0; i < size; i++) b[i] = 0;
double * c = new double[size];
for(size_t i = 0; i < size; i++) c[i] = 0;

當然,程序集現在將這些循環優化為memset調用,與std::unique_ptr<double[]>版本相同! 現在運行時具有可比性。

(注意:指針的運行時間現在比其他兩個慢!我觀察到第一個被調用的函數,無論哪一個,總是慢約200ms-400ms。我指責分支預測。無論哪種方式,速度應該現在在所有三個代碼路徑中都是相同的)。

這就是教訓: std::vectorstd::unique_ptr通過阻止您在使用原始指針的代碼中調用的未定義行為,使您的代碼更安全一些。 結果是它也使你的代碼變慢。

觀察到的行為不是特定於OpenMP的,而是與現代操作系統管理內存的方式有關。 內存是虛擬的,這意味着每個進程都有自己的虛擬地址(VA)空間,並且使用特殊的轉換機制將該VA空間的頁面映射到物理內存的幀。 因此,內存分配分兩個階段執行:

  • 在VA空間內保留一個區域 - 當分配足夠大時,這就是operator new[]所做的事情(由於效率原因,較小的分配處理方式不同)
  • 在訪問該地區的某些部分時,實際上用物理內存支持該區域

該過程分為兩部分,因為在許多情況下,應用程序不會立即使用它們保留的所有內存,並且使用物理內存備份整個預留可能會導致浪費(與虛擬內存不同,物理內存是非常有限的資源)。 因此,在進程首次寫入分配的存儲空間的區域時,按需執行對物理存儲器的后備保留。 該過程被稱為故障內存區域,因為在大多數體系結構中它涉及軟頁面錯誤,觸發OS內核內的映射。 每當您的代碼第一次寫入仍未由物理內存支持的內存區域時,就會觸發軟頁面錯誤,操作系統會嘗試映射物理頁面。 該過程很慢,因為它涉及在流程頁表上查找空閑頁面和修改。 除非有某種大頁面機制,例如Linux上的透明大頁面機制,否則該過程的典型粒度為4 KiB。

如果您是第一次從一個從未寫過的頁面中讀取,會發生什么? 同樣,發生軟頁面錯誤,但Linux內核不是映射物理內存幀,而是映射一個特殊的“零頁面”。 頁面以CoW(寫時復制)模式映射,這意味着當您嘗試編寫它時,映射到零頁面將被映射到新的物理內存幀。

現在,看看數組的大小。 abca占用80 MB,這超過了大多數現代CPU的高速緩存大小。 因此,並行循環的一次執行必須從主存儲器帶來160MB的數據並寫回80MB。 由於系統緩存的工作原理,寫入c實際上只讀取一次,除非使用非時間(緩存旁路)存儲,因此讀取240 MB數據並寫入80 MB數據。 乘以200次外迭代,總共可以讀取48 GB的數據和16 GB的數據。

上面的情況並非如此時ab未初始化,即情況下,當ab被簡單地使用分配operator new[] 由於在這種情況下的讀取導致訪問零頁面,並且物理上只有一個零頁面容易適合CPU高速緩存,因此不必從主存儲器引入實際數據。 因此,只需要讀入16 GB的數據然后再寫回。 如果使用非臨時存儲,則根本不讀取任何內存。

這可以使用LIKWID(或任何其他能夠讀取CPU硬件計數器的工具)輕松證明:

std::vector<double>版本:

$ likwid-perfctr -C 0 -g HA a.out
...
+-----------------------------------+------------+
|               Metric              |   Core 0   |
+-----------------------------------+------------+
|        Runtime (RDTSC) [s]        |     4.4796 |
|        Runtime unhalted [s]       |     5.5242 |
|            Clock [MHz]            |  2850.7207 |
|                CPI                |     1.7292 |
|  Memory read bandwidth [MBytes/s] | 10753.4669 |
|  Memory read data volume [GBytes] |    48.1715 | <---
| Memory write bandwidth [MBytes/s] |  3633.8159 |
| Memory write data volume [GBytes] |    16.2781 |
|    Memory bandwidth [MBytes/s]    | 14387.2828 |
|    Memory data volume [GBytes]    |    64.4496 | <---
+-----------------------------------+------------+

帶有未初始化數組的版本:

+-----------------------------------+------------+
|               Metric              |   Core 0   |
+-----------------------------------+------------+
|        Runtime (RDTSC) [s]        |     2.8081 |
|        Runtime unhalted [s]       |     3.4226 |
|            Clock [MHz]            |  2797.2306 |
|                CPI                |     1.0753 |
|  Memory read bandwidth [MBytes/s] |  5696.4294 |
|  Memory read data volume [GBytes] |    15.9961 | <---
| Memory write bandwidth [MBytes/s] |  5703.4571 |
| Memory write data volume [GBytes] |    16.0158 |
|    Memory bandwidth [MBytes/s]    | 11399.8865 |
|    Memory data volume [GBytes]    |    32.0119 | <---
+-----------------------------------+------------+

具有未初始化數組和非臨時存儲的版本(使用Intel的#pragma vector nontemporal ):

+-----------------------------------+------------+
|               Metric              |   Core 0   |
+-----------------------------------+------------+
|        Runtime (RDTSC) [s]        |     1.5889 |
|        Runtime unhalted [s]       |     1.7397 |
|            Clock [MHz]            |  2530.1640 |
|                CPI                |     0.5465 |
|  Memory read bandwidth [MBytes/s] |   123.4196 |
|  Memory read data volume [GBytes] |     0.1961 | <---
| Memory write bandwidth [MBytes/s] | 10331.2416 |
| Memory write data volume [GBytes] |    16.4152 |
|    Memory bandwidth [MBytes/s]    | 10454.6612 |
|    Memory data volume [GBytes]    |    16.6113 | <---
+-----------------------------------+------------+

在使用GCC 5.3時,在您的問題中提供的兩個版本的反匯編表明,兩個循環被轉換為完全相同的匯編指令序列,而不是代碼地址。 執行時間不同的唯一原因是如上所述的存儲器訪問。 調整向量的大小會用零初始化它們,這會導致ab由它們自己的物理內存頁面備份。 當使用operator new[]時,不初始化ab會導致它們被零頁面支持。

編輯:我花了這么長時間寫這篇文章,同時祖蘭寫了一個更技術性的解釋方法。

我測試了它並發現了以下內容: vector大小寫的運行時間比原始數組大約長1.8倍。 但這只是我沒有初始化原始數組的情況。 在時間測量之前添加一個簡單的循環以初始化所有具有0.0的條目時,原始數組的情況與vector情況一樣長。

仔細觀察並做了以下事情:我沒有初始化原始數組,如

for (size_t i{0}; i < SIZE; ++i)
    a[i] = 0.0;

但是這樣做了:

for (size_t i{0}; i < SIZE; ++i)
    if (a[i] != 0.0)
    {
        std::cout << "a was set at position " << i << std::endl;
        a[i] = 0.0;
    }

(相應的其他數組)。
結果是我沒有從初始化數組得到控制台輸出,它再次沒有初始化那么快,這比使用vector s快約1.8。

當我初始化例如只有a “正常”而另外兩個向量帶有if子句時,我測量了vector運行時和運行時之間的時間,所有數組都用if子句“偽初始化”。

嗯......那很奇怪......

現在,我認為std :: vector幾乎沒有開銷? 這里發生了什么? 我想用漂亮的STL對象......

雖然我無法解釋你這種行為,但我可以告訴你,如果你使用它“正常”, std::vector並沒有真正的開銷。 這只是一個非常人為的案例。

編輯:

正如qPCR4vir和OP Napseis指出的那樣,這可能與優化有關。 一旦我打開優化,“真正初始化”的情況就是已經提到的1.8慢的因素。 但沒有它仍然慢約1.1倍。

所以我查看了匯編程序代碼,但我沒有看到'for'循環有任何區別......

這里要注意的主要事實是

陣列版本具有未定義的行為

dcl.init#12州:

如果評估產生不確定的值,則行為未定義

這正是該行中發生的事情:

c[i] = a[i] + b[i];

a[i]b[i]都是不確定的值,因為數組是默認初始化的。

UB完美地解釋了測量結果(無論它們是什么)。

UPD :根據@HristoIliev和@Zulan的回答,我想再次強調語言POV。

為編譯器讀取未初始化內存的UB本質上意味着它總是可以假定內存已初始化,因此無論操作系統如何處理C ++都可以,即使操作系統對該情況具有某些特定行為。

事實證明它確實 - 你的代碼沒有讀取物理內存,你的測量結果與之相符。

可以說結果程序不會計算兩個數組的總和 - 它計算兩個更容易訪問的模擬的總和,而C ++正好因為UB。 如果它做了別的事情,它仍然會完全沒問題。

所以最后你有兩個程序:一個加起來兩個向量,另一個只做一些未定義的東西(從C ++的角度來看)或不相關的東西(從OS的角度來看)。 測量他們的時間並比較結果有什么意義?

修復UB解決了整個問題,但更重要的是它驗證了您的測量結果並允許您有意義地比較結果。

在這種情況下,我認為罪魁禍首是-funroll-loops,來自我在O2中使用和不使用此選項進行測試的內容。

https://gcc.gnu.org/onlinedocs/gcc-5.4.0/gcc/Optimize-Options.html#Optimize-Options

funroll-loops:展開循環,其迭代次數可以在編譯時或進入循環時確定。 -funroll-loops意味着-frerun-cse-after-loop。 它還打開完全循環剝離(即完全去除具有小的恆定迭代次數的循環)。 此選項使代碼變大,可能會也可能不會使代碼運行得更快。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM