Strlen sẽ được tính nhiều lần nếu được sử dụng trong một điều kiện lặp lại?


109

Tôi không chắc liệu mã sau có thể gây ra các phép tính dư thừa hay nó dành riêng cho trình biên dịch?

for (int i = 0; i < strlen(ss); ++i)
{
    // blabla
}

Sẽ strlen()được tính toán mỗi khi ităng?


14
Tôi sẽ đoán rằng nếu không có một tối ưu hóa phức tạp có thể phát hiện 'ss' đó không bao giờ thay đổi trong vòng lặp, thì có. Tốt nhất bạn nên biên dịch và nhìn vào assembly để xem.
MerickOWA

6
Nó phụ thuộc vào trình biên dịch, vào mức độ tối ưu hóa và những gì bạn (có thể) làm ssbên trong vòng lặp.
Hristo Iliev

4
Nếu trình biên dịch có thể chứng minh điều đó sskhông bao giờ được sửa đổi, nó có thể đưa tính toán ra khỏi vòng lặp.
Daniel Fischer

10
@Mike: "yêu cầu phân tích thời gian biên dịch về chính xác những gì strlen làm" - strlen có thể là một nội tại, trong trường hợp đó, trình tối ưu hóa biết nó làm gì.
Steve Jessop

3
@MikeSeymour: Có thể không, có thể không. strlen được định nghĩa bởi tiêu chuẩn ngôn ngữ C, và tên của nó được dành riêng cho việc sử dụng theo định nghĩa của ngôn ngữ, vì vậy một chương trình không được tự do cung cấp một định nghĩa khác. Trình biên dịch và trình tối ưu hóa được quyền giả định strlen chỉ phụ thuộc vào đầu vào của nó và không sửa đổi nó hoặc bất kỳ trạng thái toàn cục nào. Thách thức để tối ưu hóa ở đây là xác định rằng bộ nhớ được trỏ đến bởi ss không bị thay đổi bởi bất kỳ mã nào bên trong vòng lặp. Điều đó hoàn toàn khả thi với các trình biên dịch hiện tại, tùy thuộc vào từng đoạn mã cụ thể.
Eric Postpischil

Câu trả lời:


138

Có, strlen()sẽ được đánh giá trên mỗi lần lặp. Có thể trong những trường hợp lý tưởng, người tối ưu có thể suy ra rằng giá trị sẽ không thay đổi, nhưng cá nhân tôi sẽ không dựa vào điều đó.

Tôi muốn làm một cái gì đó giống như

for (int i = 0, n = strlen(ss); i < n; ++i)

hoặc có thể

for (int i = 0; ss[i]; ++i)

miễn là chuỗi sẽ không thay đổi độ dài trong quá trình lặp. Nếu nó có thể xảy ra, thì bạn sẽ cần phải gọi strlen()mỗi lần hoặc xử lý nó thông qua logic phức tạp hơn.


14
Nếu bạn biết mình không thao tác trên chuỗi, thì cách thứ hai thích hợp hơn nhiều vì đó về cơ bản là vòng lặp sẽ được thực hiện bởi strlendù sao.
mlibby

26
@alk: Nếu chuỗi có thể bị rút ngắn, thì cả hai điều này đều sai.
Mike Seymour

3
@alk: nếu bạn đang thay đổi chuỗi, vòng lặp for có lẽ không phải là cách tốt nhất để lặp lại từng ký tự. Tôi nghĩ rằng vòng lặp while trực tiếp hơn và dễ quản lý bộ đếm chỉ mục hơn.
mlibby

2
các trường hợp lý tưởng bao gồm biên dịch với GCC trong linux, nơi strlenđược đánh dấu là __attribute__((pure))cho phép trình biên dịch thực hiện nhiều cuộc gọi. Các thuộc tính của GCC
David Rodríguez - rê bóng vào

6
Phiên bản thứ hai là hình thức lý tưởng và dễ thành ngữ nhất. Nó cho phép bạn chuyển qua chuỗi chỉ một lần thay vì hai lần, điều này sẽ có hiệu suất tốt hơn nhiều (đặc biệt là đồng tiền mã hóa bộ nhớ cache) cho các chuỗi dài.
R .. GitHub DỪNG TRỢ GIÚP NGAY LÚC NÀY

14

Có, mỗi khi bạn sử dụng vòng lặp. Sau đó, nó sẽ mỗi lần tính toán độ dài của chuỗi. vì vậy hãy sử dụng nó như thế này:

char str[30];
for ( int i = 0; str[i] != '\0'; i++)
{
//Something;
}

Trong đoạn mã trên str[i]chỉ xác minh một ký tự cụ thể trong chuỗi tại vị trí imỗi khi vòng lặp bắt đầu một chu kỳ, do đó nó sẽ tốn ít bộ nhớ hơn và hiệu quả hơn.

Xem Liên kết này để biết thêm thông tin.

Trong đoạn mã dưới đây mỗi khi vòng lặp chạy strlensẽ đếm độ dài của cả chuỗi kém hiệu quả hơn, tốn nhiều thời gian hơn và tốn nhiều bộ nhớ hơn.

char str[];
for ( int i = 0; i < strlen(str); i++)
{
//Something;
}

3
Tôi có thể đồng ý với "[nó] hiệu quả hơn", nhưng sử dụng ít bộ nhớ hơn? Sự khác biệt duy nhất về việc sử dụng bộ nhớ mà tôi có thể nghĩ đến sẽ nằm trong ngăn xếp cuộc gọi trong khi strlengọi và nếu bạn đang hoạt động chặt chẽ như vậy, có lẽ bạn cũng nên suy nghĩ về việc bỏ qua một vài lệnh gọi hàm khác ...
CVn

@ MichaelKjörling Nếu bạn sử dụng "strlen", thì trong một vòng lặp, nó phải quét toàn bộ chuỗi mỗi khi vòng lặp chạy, trong khi trong đoạn mã trên là "str [ix]", nó chỉ quét một phần tử trong mỗi chu kỳ của vòng lặp có vị trí được đại diện bởi "ix". Do đó, nó chiếm ít bộ nhớ hơn "strlen".
codeDEXTER

1
Tôi không chắc điều đó thực sự có ý nghĩa. Một triển khai rất ngây thơ của strlen sẽ là một cái gì đó giống như int strlen(char *s) { int len = 0; while(s[len] != '\0') len++; return len; }chính xác những gì bạn đang làm trong mã trong câu trả lời của bạn. Tôi không tranh luận rằng việc lặp lại chuỗi một lần thay vì hai lần sẽ tiết kiệm thời gian hơn , nhưng tôi không thấy cái này hay cái kia sử dụng nhiều hơn hoặc ít bộ nhớ hơn. Hay bạn đang đề cập đến biến được sử dụng để giữ độ dài chuỗi?
một CVn

@ MichaelKjörling Vui lòng xem mã đã chỉnh sửa ở trên và liên kết. Và đối với bộ nhớ - mỗi khi vòng lặp chạy, mỗi giá trị lặp đi lặp lại được lưu trữ trong bộ nhớ và trong trường hợp 'strlen' vì nó đếm đi đếm lại toàn bộ chuỗi, nó đòi hỏi nhiều bộ nhớ hơn để lưu trữ. và cũng bởi vì không giống như Java, C ++ không có "Garbage Collector". Sau đó, tôi cũng có thể sai. xem liên kết về sự vắng mặt của "Garbage Collector" trong C ++.
codeDEXTER

1
@ aashis2s Việc thiếu bộ thu gom rác chỉ đóng vai trò khi tạo các đối tượng trên heap. Các đối tượng trên ngăn xếp sẽ bị phá hủy ngay khi phạm vi và kết thúc.
Ikke

9

Một trình biên dịch tốt có thể không tính toán nó mọi lúc, nhưng tôi không nghĩ bạn có thể chắc chắn rằng mọi trình biên dịch đều làm được điều đó.

Thêm vào đó, trình biên dịch phải biết, điều strlen(ss)đó không thay đổi. Điều này chỉ đúng nếu sskhông được thay đổi trong forvòng lặp.

Ví dụ: nếu bạn sử dụng một hàm chỉ đọc sstrong forvòng lặp nhưng không khai báo ss-parameter const, trình biên dịch thậm chí không thể biết rằng ssnó không được thay đổi trong vòng lặp và phải tính toán strlen(ss)trong mỗi lần lặp.


3
+1: Không những sskhông được thay đổi trong forvòng lặp; nó không được truy cập và thay đổi bởi bất kỳ hàm nào được gọi trong vòng lặp (vì nó được truyền dưới dạng đối số hoặc vì nó là biến toàn cục hoặc biến phạm vi tệp). Chứng chỉ hằng số cũng có thể là một yếu tố.
Jonathan Leffler

4
Tôi nghĩ rằng rất khó có khả năng trình biên dịch có thể biết rằng 'ss' không thay đổi. Có thể có con trỏ đi lạc điểm đó vào bộ nhớ bên trong 'ss' mà trình biên dịch không có ý tưởng về điều đó có thể thay đổi 'ss'
MerickOWA

Jonathan đúng, một chuỗi const cục bộ có thể là cách duy nhất để trình biên dịch đảm bảo rằng không có cách nào để 'ss' thay đổi.
MerickOWA

2
@MerickOWA: thực sự, đó là một trong những thứ restrictdành cho C99.
Steve Jessop

4
Về đoạn cuối cùng của bạn: nếu bạn gọi một hàm chỉ đọc sstrong vòng lặp for, thì ngay cả khi tham số của nó được khai báo const char*, trình biên dịch vẫn cần tính toán lại độ dài trừ khi (a) nó biết rằng nó sstrỏ đến đối tượng const, trái ngược với việc chỉ là một con trỏ đến const, hoặc (b) nó có thể nội dòng hàm hoặc nói cách khác rằng nó ở chế độ chỉ đọc. Lấy một const char*tham số không phải là một lời hứa sẽ không sửa đổi dữ liệu được trỏ tới, vì nó hợp lệ để truyền đến char*và sửa đổi với điều kiện là đối tượng được sửa đổi không phải là const và không phải là một chuỗi ký tự.
Steve Jessop

4

Nếu ssthuộc loại const char *và bạn không loại bỏ sự constphức tạp trong vòng lặp, trình biên dịch có thể chỉ gọi strlenmột lần, nếu tính năng tối ưu hóa được bật. Nhưng đây chắc chắn không phải là hành vi có thể được tính đến.

Bạn nên lưu strlenkết quả trong một biến và sử dụng biến này trong vòng lặp. Nếu bạn không muốn tạo một biến bổ sung, tùy thuộc vào những gì bạn đang làm, bạn có thể thoát khỏi việc đảo ngược vòng lặp để lặp lại.

for( auto i = strlen(s); i > 0; --i ) {
  // do whatever
  // remember value of s[strlen(s)] is the terminating NULL character
}

1
Đó là một sai lầm khi gọi điện strlen. Chỉ cần lặp lại cho đến khi bạn đạt kết thúc.
R .. GitHub DỪNG TRỢ GIÚP NGAY LÚC NÀY

i > 0? Điều đó không nên i >= 0ở đây? Cá nhân tôi cũng sẽ bắt đầu strlen(s) - 1nếu lặp lại chuỗi ngược lại, thì việc kết thúc \0không cần xem xét đặc biệt.
một CVn

2
@ MichaelKjörling i >= 0chỉ hoạt động nếu bạn khởi tạo để strlen(s) - 1, nhưng sau đó nếu bạn có một chuỗi trên zero chiều dài underflows giá trị ban đầu
thuộc về pháp quan

@ Prætorian, điểm tốt trên chuỗi độ dài bằng không. Tôi đã không xem xét trường hợp đó khi tôi viết bình luận của mình. C ++ có đánh giá i > 0biểu thức trên mục nhập vòng lặp ban đầu không? Nếu không, thì bạn nói đúng, trường hợp zero length chắc chắn sẽ phá vỡ vòng lặp. Nếu đúng như vậy, bạn "đơn giản" nhận được dấu i== -1 <0 để không có mục nhập vòng lặp nếu điều kiện là i >= 0.
một CVn

@ MichaelKjörling Có, điều kiện thoát được đánh giá trước khi thực hiện vòng lặp lần đầu tiên. strlenKiểu trả về của không có dấu, vì vậy (strlen(s)-1) >= 0giá trị là true đối với các chuỗi có độ dài bằng không.
Praetorian

3

Về mặt chính thức là có, strlen()dự kiến ​​sẽ được gọi cho mỗi lần lặp lại.

Dù sao, tôi không muốn phủ nhận khả năng tồn tại của một số tối ưu hóa trình biên dịch thông minh, điều đó sẽ tối ưu hóa bất kỳ lệnh gọi liên tiếp nào đến strlen () sau lệnh đầu tiên.


3

Toàn bộ mã vị từ sẽ được thực thi trên mỗi lần lặp lại của forvòng lặp. Để ghi nhớ kết quả của strlen(ss)cuộc gọi, trình biên dịch cần biết rằng ít nhất

  1. Chức năng không strlencó tác dụng phụ
  2. Bộ nhớ được trỏ đến sskhông thay đổi trong suốt thời gian của vòng lặp

Trình biên dịch không biết một trong hai điều này và do đó không thể ghi nhớ một cách an toàn kết quả của lần gọi đầu tiên


có thể biết những điều đó với phân tích tĩnh, nhưng tôi nghĩ rằng quan điểm của bạn là phân tích như vậy hiện không được thực hiện trong bất kỳ trình biên dịch C ++ nào, phải không?
GManNickG

@GManNickG nó chắc chắn có thể chứng minh # 1 nhưng # 2 khó hơn. Đối với một luồng duy nhất, nó chắc chắn có thể chứng minh điều đó nhưng không phải đối với môi trường đa luồng.
JaredPar

1
Có lẽ tôi đang cứng đầu nhưng tôi nghĩ số hai cũng có thể xảy ra trong môi trường đa luồng, nhưng chắc chắn không phải là không có một hệ thống suy luận cực kỳ mạnh mẽ. Chỉ đang trầm ngâm ở đây; chắc chắn nằm ngoài phạm vi của bất kỳ trình biên dịch C ++ nào hiện tại.
GManNickG

@GManNickG Tôi không nghĩ là có thể thực hiện được trong C / C ++. Tôi có thể rất dễ dàng lưu trữ địa chỉ của ssthành một size_thoặc chia nó thành một số bytegiá trị. Chủ đề quanh co của tôi sau đó có thể chỉ cần ghi các byte vào địa chỉ đó và trình biên dịch sẽ biết cách hiểu mà nó liên quan đến ss.
JaredPar

1
@JaredPar: Rất tiếc, bạn có thể khẳng định rằng int a = 0; do_something(); printf("%d",a);không thể tối ưu hóa, trên cơ sở do_something()có thể thực hiện nội dung chưa được khởi tạo của bạn hoặc có thể thu thập dữ liệu sao lưu ngăn xếp và sửa đổi có achủ ý. Trong thực tế, gcc 4.5 không tối ưu hóa nó để do_something(); printf("%d",0);với O3
Steve Jessop

2

Vâng . strlen sẽ được tính toán mọi lúc khi tôi tăng.

Nếu bạn không thay đổi ss bằng trong vòng lặp có nghĩa là nó sẽ không ảnh hưởng đến logic nếu không nó sẽ ảnh hưởng.

Sẽ an toàn hơn khi sử dụng mã sau.

int length = strlen(ss);

for ( int i = 0; i < length ; ++ i )
{
 // blabla
}

2

Có, strlen(ss)sẽ tính toán độ dài ở mỗi lần lặp. Nếu bạn đang tăng sstheo một cách nào đó và cũng đang tăng i; sẽ có vòng lặp vô hạn.


2

Có, strlen()hàm được gọi mỗi khi vòng lặp được đánh giá.

Nếu bạn muốn nâng cao hiệu quả thì hãy luôn nhớ lưu mọi thứ trong các biến cục bộ ... Sẽ mất thời gian nhưng nó rất hữu ích ..

Bạn có thể sử dụng mã như dưới đây:

String str="ss";
int l = strlen(str);

for ( int i = 0; i < l ; i++ )
{
    // blablabla
}


2

Ngày nay không phổ biến nhưng 20 năm trước trên các nền tảng 16 bit, tôi khuyên bạn nên sử dụng điều này:

for ( char* p = str; *p; p++ ) { /* ... */ }

Ngay cả khi trình biên dịch của bạn không quá thông minh trong việc tối ưu hóa, đoạn mã trên có thể dẫn đến mã lắp ráp tốt.


1

Đúng. Kiểm tra không biết rằng ss không bị thay đổi bên trong vòng lặp. Nếu bạn biết rằng nó sẽ không thay đổi thì tôi sẽ viết:

int stringLength = strlen (ss); 
for ( int i = 0; i < stringLength; ++ i ) 
{
  // blabla 
} 

1

Arrgh, nó sẽ, ngay cả trong những hoàn cảnh lý tưởng, chết tiệt!

Kể từ hôm nay (tháng 1 năm 2018), và gcc 7.3 và clang 5.0, nếu bạn biên dịch:

#include <string.h>

void bar(char c);

void foo(const char* __restrict__ ss) 
{
    for (int i = 0; i < strlen(ss); ++i) 
    {
        bar(*ss);
    }
}    

Vì vậy chúng tôi có:

  • ss là một con trỏ hằng.
  • ss Chấm điểm __restrict__
  • Phần thân vòng lặp không thể chạm vào bộ nhớ được trỏ tới theo bất kỳ cách nào ss(tốt, trừ khi nó vi phạm __restrict__).

vẫn còn , cả hai trình biên dịch đều thực thi strlen() mọi lần lặp lại của vòng lặp đó . Kinh ngạc.

Điều này cũng có nghĩa là những ám chỉ / mơ tưởng về @Praetorian và @JaredPar không xuất hiện.


0

CÓ, nói một cách đơn giản. Và hiếm có điều kiện nhỏ nào mà trình biên dịch mong muốn, như một bước tối ưu hóa nếu nó nhận thấy rằng không có thay đổi nào được thực hiện ss. Nhưng trong điều kiện an toàn, bạn nên nghĩ là CÓ. Có một số tình huống như trong multithreadedvà chương trình điều khiển sự kiện, nó có thể bị lỗi nếu bạn coi đó là KHÔNG. Chơi an toàn vì nó sẽ không cải thiện độ phức tạp của chương trình quá nhiều.


0

Đúng.

strlen()tính toán mọi lúc khi ităng và không được tối ưu hóa.

Đoạn mã dưới đây cho thấy lý do tại sao trình biên dịch không nên tối ưu hóa strlen().

for ( int i = 0; i < strlen(ss); ++i )
{
   // Change ss string.
   ss[i] = 'a'; // Compiler should not optimize strlen().
}

Tôi nghĩ rằng thực hiện sửa đổi cụ thể đó không bao giờ thay đổi độ dài của ss, chỉ là nội dung của nó, vì vậy (một trình biên dịch thực sự, thực sự thông minh) vẫn có thể tối ưu hóa strlen.
Darren Cook

0

Chúng tôi có thể dễ dàng kiểm tra nó:

char nums[] = "0123456789";
size_t end;
int i;
for( i=0, end=strlen(nums); i<strlen(nums); i++ ) {
    putchar( nums[i] );
    num[--end] = 0;
}

Điều kiện vòng lặp đánh giá sau mỗi lần lặp lại, trước khi bắt đầu lại vòng lặp.

Ngoài ra, hãy cẩn thận về loại bạn sử dụng để xử lý độ dài của chuỗi. nó phải size_tđược định nghĩa như unsigned inttrong stdio. so sánh và truyền nó tới intcó thể gây ra một số vấn đề về lỗ hổng nghiêm trọng.


0

tốt, tôi nhận thấy rằng ai đó đang nói rằng nó được tối ưu hóa theo mặc định bởi bất kỳ trình biên dịch hiện đại "thông minh" nào. Nhân tiện nhìn vào kết quả mà không cần tối ưu hóa. Tôi đã thử:
Mã C tối thiểu:

#include <stdio.h>
#include <string.h>

int main()
{
 char *s="aaaa";

 for (int i=0; i<strlen(s);i++)
  printf ("a");
 return 0;
}

Trình biên dịch của tôi: g ++ (Ubuntu / Linaro 4.6.3-1ubuntu5) 4.6.3
Lệnh để tạo mã lắp ráp: g ++ -S -masm = intel test.cpp

Gotten assembly code at the output:
    ...
    L3:
mov DWORD PTR [esp], 97
call    putchar
add DWORD PTR [esp+40], 1
    .L2:
     THIS LOOP IS HERE
    **<b>mov    ebx, DWORD PTR [esp+40]
mov eax, DWORD PTR [esp+44]
mov DWORD PTR [esp+28], -1
mov edx, eax
mov eax, 0
mov ecx, DWORD PTR [esp+28]
mov edi, edx
repnz scasb</b>**
     AS YOU CAN SEE it's done every time
mov eax, ecx
not eax
sub eax, 1
cmp ebx, eax
setb    al
test    al, al
jne .L3
mov eax, 0
     .....

Tôi sẽ không hài lòng khi tin tưởng bất kỳ trình biên dịch nào đã cố gắng tối ưu hóa nó trừ khi địa chỉ của chuỗi được- restrictđủ điều kiện. Mặc dù có một số trường hợp việc tối ưu hóa như vậy là hợp pháp, nhưng nỗ lực cần thiết để xác định một cách đáng tin cậy các trường hợp như vậy restrict, bằng bất kỳ biện pháp hợp lý nào, gần như chắc chắn sẽ vượt quá lợi ích. const restrictTuy nhiên, nếu địa chỉ của chuỗi có một định tính , điều đó sẽ đủ để chứng minh cho việc tối ưu hóa mà không cần phải xem xét bất kỳ điều gì khác.
supercat

0

Xây dựng câu trả lời của Prætorian, tôi khuyên bạn nên làm như sau:

for( auto i = strlen(s)-1; i > 0; --i ) {foo(s[i-1];}
  • autobởi vì bạn không muốn quan tâm đến việc trả về kiểu strlen nào. Trình biên dịch C ++ 11 (ví dụ: gcc -std=c++0xkhông hoàn toàn C ++ 11 nhưng các kiểu tự động hoạt động) sẽ làm điều đó cho bạn.
  • i = strlen(s)vì bạn muốn so sánh với 0(xem bên dưới)
  • i > 0 bởi vì so sánh với 0 nhanh hơn (một chút) so với bất kỳ số nào khác.

bất lợi là bạn phải sử dụng i-1để truy cập các ký tự chuỗi.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.