char array[] = "string" を実行すると、文字列リテラル "string" がデータ セグメントからスタックにコピーされることを理解しています。文字列リテラルは文字ごとにコピーされますか? または、コンパイラは文字列リテラルの開始アドレスと終了アドレスを取得し、文字列全体を一度にスタックにコピーしますか?
ありがとう
コンパイラは、観測結果が同じである限り、「やりたい」ことは何でもします。コピーがまったくない場合もあります。
C 標準では、コピーの実行方法が指定されていないため、C の実装では、どのような方法でも自由に結果を得ることができます。C 標準が課す唯一の要件は、観察可能な結果 (標準出力に書き込まれるテキストなど) が定義どおりでなければならないということです。
エンジニアが C 実装を高品質に設計する場合、そのような状況で文字列をコピーする最善の方法を検討するのに時間を費やし、それぞれの状況で最適な方法を選択するコンパイラを設計しようとします。短い文字列は、「即値の移動」命令を使用してその場で構築される場合があります。への呼び出しによって、長い文字列がコピーされる場合がありますmemcpy
。中間文字列は、 へのインライン呼び出しによってコピーされる可能性がありますmemcpy
。事実上、それぞれ数バイトを移動するいくつかの命令です。
エンジニアが安価な C 実装を設計している場合、コードをマシンに移植できるようにするだけで、高速である必要はありません。
コンパイラが文字列をまったくコピーしない場合があります。コピーが必要ないとコンパイラが判断できる場合、コピーを作成する理由はありません。たとえば、文字列を に渡すだけprintf
でまったく変更していないことをコンパイラが認識した場合、コンパイラは元の文字列を に渡すことで、コピーを作成せずに同じ結果を取得しprintf
ます。
コピーがあると考える理由はまったくありません。
たとえば、次のコードを見てください。
int main() {
char c[] = "hi";
}
私にとって、これは(最適化されていない)アセンブリを生成します:
main:
pushq %rbp
movq %rsp, %rbp
movw $26984, -16(%rbp)
movb $0, -14(%rbp)
movl $0, %eax
popq %rbp
ret
配列のメモリは、値 26984 に設定することによって初期化されます。この値は、'h' と 'i' の ASCII 値である 0x68 と 0x69 の 2 バイトで表されます。文字列のデータ セグメント表現はまったくなく、文字単位で何かをコピーしたり、その他の巧妙なコピー方法によって配列を初期化したりすることはありません。
もちろん、これは 1 つのコンパイラの実装 (g++ 4.8) にすぎず、言語仕様に準拠している限り、他のコンパイラは好きなことを行うことができます。
「文字ごと」と「文字列全体」のコピー方法の違いが何を意味するのかわかりません。通常、文字列はマシンレベルのエンティティではありません。つまり、「文字列全体」としてコピーされる可能性はありません。これがどのように起こると思いますか?
少なくとも概念的には、文字列は常に「文字ごと」にコピーされます。現在、拡張メモリ領域のコピーに関しては、可能な限り (バイト単位ではなく) 単語単位でコピーを実行することにより、コンパイラによってコピー プロセスを最適化できます。同様の最適化が、プロセッサのマイクロアーキテクチャ レベルで実装される可能性があります。
とにかく、一般的な場合、コピーは「文字列全体」に対するアトミック操作としてではなく、反復プロセスとして実装されます。
その上、スマートなコンパイラは、場合によってはコピーがまったく必要ないことに気付くかもしれません。たとえば、コードがarray
オブジェクトを変更せず、そのアドレス ID に依存しない場合、コンパイラは、コピーをまったく行わずに元の文字列リテラルを直接使用することを決定する可能性があります (つまり、基本的に静かに置き換えchar array[] = "string"
ますconst char *array = "string"
)
これは、コンパイラとターゲット アーキテクチャによって異なります。
メモリ ブロックのコピーをサポートする命令を持たない、マイクロコントローラーなどの非常に単純なターゲット アーキテクチャが存在する可能性があります。教育用に設計された非常に単純なコンパイラがおそらく存在し、より効果的な方法をサポートするアーキテクチャ上でさえ、バイトごとのコピーを生成します。
ただし、この場合、製品レベルのコンパイラは合理的なことを行い、ほとんどの一般的なアーキテクチャで可能な限り高速なコードを生成すると想定でき、実際に心配する必要はありません。
それでも、確認する最善の方法は、コンパイラが生成するアセンブリを読み取ることです。
次のテスト コード (stack_array_init.c) を使用します。
#include <stdio.h>
int
main()
{
char a[]="Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed\n"
"do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n";
printf("%s", a);
return 0;
}
そして、次のように、サイズを最適化して(読み取る量を減らすために)gccでアセンブリにコンパイルします。
gcc -Os -S stack_array_init.c
x86-64 の出力は次のとおりです。
.file "stack_array_init.c"
.section .rodata.str1.1,"aMS",@progbits,1
.LC1:
.string "%s"
.LC0:
.string "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed\ndo eiusmod tempor incididunt ut labore et dolore magna aliqua.\n"
.section .text.startup,"ax",@progbits
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
subq $136, %rsp
.cfi_def_cfa_offset 144
movl $.LC0, %esi
movl $126, %ecx
leaq 2(%rsp), %rdi
xorl %eax, %eax
rep movsb
leaq 2(%rsp), %rsi
movl $.LC1, %edi
call printf
xorl %eax, %eax
addq $136, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Debian 4.7.2-5) 4.7.2"
.section .note.GNU-stack,"",@progbits
ここで、rep movsb は文字列をスタックにコピーする命令です。
以下は、ARMv4 アセンブリからの抜粋です (読みやすいかもしれません)。
main:
@ Function supports interworking.
@ args = 0, pretend = 0, frame = 128
@ frame_needed = 0, uses_anonymous_args = 0
str lr, [sp, #-4]!
sub sp, sp, #132
mov r2, #126
ldr r1, .L2
mov r0, sp
bl memcpy
mov r1, sp
ldr r0, .L2+4
bl printf
mov r0, #0
add sp, sp, #132
ldr lr, [sp], #4
bx lr
.L3:
.align 2
.L2:
.word .LC0
.word .LC1
.size main, .-main
.section .rodata.str1.4,"aMS",%progbits,1
.align 2
.LC1:
.ascii "%s\000"
.space 1
.LC0:
.ascii "Lorem ipsum dolor sit amet, consectetur adipisicing"
.ascii " elit, sed\012do eiusmod tempor incididunt ut labor"
.ascii "e et dolore magna aliqua.\012\000"
.ident "GCC: (Debian 4.6.3-14) 4.6.3"
.section .note.GNU-stack,"",%progbits
私のARMアセンブリの理解では、これはコードがmemcpyを呼び出して文字列をスタック配列にコピーしているように見えます。これは memcpy のアセンブリを示していませんが、利用可能な最速の方法の 1 つを使用することを期待しています。