모두의 코드 커뮤니티

C++ 변수의 정의와 변수 값 선언

for문 안에서 변수에 어떤 값을 대입하고 사용할 일이 있습니다.
여기서 vec은 값이 잘 정의된 어떤 vector container입니다.

(1)
for ( ~~ )
{
int val = vec[i];
(working with val)
}
(2)
int val;
for (~~)
{
val = vec[i];
(working with val)
}

제가 생각하기에 (1)의 경우 매번 반복될 때마다 val에게 메모리를 새로 할당할 것이라고 생각했습니다.
하지만 몇번을 반복하더라도 (2)경우와 같이 val의 주소값이 항상 같더라고요.
제가 뭔가 잘 못 이해하고 있는 걸까요 ??

그리고 만약에 하단과 같은 코드가 있다면
(3)
for (~~)
{
const int& val = vec[i];
(working with val)
}

(1)의 경우 vec[i]의 값이 임시객체에 저장되고 val을 위한 메모리를 할당하고 임시객체의 값을 val에 할당된 메모리에 복사하고 임시객체는 소멸할 것이라고 생각했고

(3)의 경우 vec[i]의 값이 임시객체에 저장되고 val이 그 임시객체의 reference가 되고 임시객체는 for문이 끝날때 까지 소멸하지 않는것이라고 생각했습니다.

따라서 (3)의 경우 별도의 메모리 할당을 할 필요가 없기 때문에 (1) (2)의 경우보다 훨씬 빠를꺼라고 생각했습니다.

하지만 이를 확인해본 결과 속도의 차이가 발생하지 않았습니다. 속도 측정은 window에서 release 모드를 이용해 실행하고 std::chrono를 사용했습니다.

제가 무엇을 잘 못 알고 있을까요 ??
답글 달아주시면 감사하겠습니다.

혹시 몰라 속도 측정에 사용했던 간단한 코드를 첨부하겠습니다.
int num = 100000;
std::vector vec(num, 0);
for (int i = 0; i < num; ++i)
vec[i] = i;

std::vector<int> vec1(num, 0);
std::cout << "for문안에 int 정의 \t\t";
start();
for (int i = 0; i < num; ++i)
{
	int a = vec[i];
	vec1[i] = a;
}
end();

std::vector<int> vec3(num, 0);
std::cout << "for문 밖에서 int 정의 후 선언 \t";
start();
int d;
for (int i = 0; i < num; ++i)
{
	d = vec[i];
	vec3[i] = d;
}
end();

std::vector<int> vec4(num, 0);
std::cout << "for문안에 const int& 정의 \t";
start();
for (int i = 0; i < num; ++i)
{
	const int& c = vec[i];
	vec4[i] = c;
}
end();
1 Like

일단 임시 객체 할당 이라고 하셨는데, 이 경우 지역 객체를 생성하는 것이기 때문에 스택에 생성됩니다. 스택에 생성되는 경우 new 를 통해서 힙에 메모리를 할당하는 것과 다르게 매우 간단하게 생성할 수 있습니다. (정확히 말하면 스택 포인터를 감소 시키면 됩니다). 더군다나 크기가 큰 객체가 아니라 int 이기 때문에 다른 생성자를 거칠 필요도 없어서 임시 객체를 만드는데 걸리는 시간은 거의 무시할만하다 라고 보시면 됩니다.

참고로 컴파일러는 생각보다 똑똑해서 불필요한 객체 생성을 막아줍니다. 실제로 생성된 코드를 어셈블리로 살펴보면 (https://godbolt.org/z/gXmsWQ) 아래와 같이 원소 대입 부분에 해당하는 어셈블리가 정확히 똑같음을 알 수 있습니다.

만약에 차이점을 느끼고 싶으셨다면 string 같이 객체의 생성이 비싼 원소들로 다시 테스트 해보세요.

.L13:
    mov     edx, DWORD PTR [rsi+rax] // 이 두 줄이 (1) 
    mov     DWORD PTR [rcx+rax], edx
    add     rax, 4
    cmp     rax, 400000
    jne     .L13
    lea     rcx, [rsp+12]
    lea     rdx, [rsp+112]
    mov     esi, 100000
    mov     DWORD PTR [rsp+112], 0
    lea     rdi, [rsp+80]
    call    std::vector<int, std::allocator<int> >::vector(unsigned long, int const&, std::allocator<int> const&)
    mov     rsi, QWORD PTR [rsp+16]
    mov     rcx, QWORD PTR [rsp+80]
    xor     eax, eax
.L14:
    mov     edx, DWORD PTR [rsi+rax] // 이 두 줄이 (2)
    mov     DWORD PTR [rcx+rax], edx
    add     rax, 4
    cmp     rax, 400000
    jne     .L14
    lea     rcx, [rsp+11]
    lea     rdx, [rsp+12]
    mov     esi, 100000
    mov     DWORD PTR [rsp+12], 0
    lea     rdi, [rsp+112]
    call    std::vector<int, std::allocator<int> >::vector(unsigned long, int const&, std::allocator<int> const&)
    mov     rcx, QWORD PTR [rsp+16]
    mov     rdi, QWORD PTR [rsp+112]
    xor     eax, eax
.L15:
    mov     edx, DWORD PTR [rcx+rax] // 이 두 줄이 (3)
    mov     DWORD PTR [rdi+rax], edx
    add     rax, 4
    cmp     rax, 400000
    jne     .L15
    call    operator delete(void*)
    mov     rdi, QWORD PTR [rsp+80]
    test    rdi, rdi
    je      .L16
1 Like

상세한 답변 감사합니다 !

재범님께서 제안해주신대로 string으로 다시 테스트를 해보았습니다.

그 결과 (3)방식 > (1)방식 > (2)방식 순서로 빠르게 계산되었습니다.

여기서 질문드리고 싶은게 세가지가 있습니다.

  1. (1)방식이 (2)방식보다 빠른 이유가 무엇일까요 ? (유의미한 수준의 차이가 항상 발생합니다.)

  2. (3)번 방식이 생성이 비싼 string 객체의 경우 압도적으로 빠른데, 그렇다면 for문 안에서 변수를 사용할 때 일반적으로 (3)번 방식으로 코드를 작성하는것이 바람직한 코딩습관일까요 ?
    (여기서 for문 안에서 변수의 사용은 어떤 상수(변하지 않는) 값들을 받아와 사용하는 상황을 얘기하는 것 입니다.)

3.계산 알고리즘을 코드로 구현하는데 있어서 속도 측면에서 최적화된 코드를 작성하고 싶은데 이를 위해서 무엇을 배워야 하는지 궁금합니다.
(더 나은 계산 알고리즘을 개발해서 컴퓨팅 시간을 더 줄이는것이 더 중요하겠지만, 계산 알고리즘을 코드로 구현하는데 있어서 불필요한 컴퓨팅 시간상의 loss 없이 작성하여 낭비되는 컴퓨팅 시간을 줄이고 싶습니다.)

항상 좋은 포스팅과 답변을 달아주셔서 큰 도움이 됩니다. 감사합니다!

하단은 제가 시간을 잴 때 사용했던 코드입니다.
void performance2(const std::string& txt)
{
int num = 10000000;
std::vectorstd::string vec(num,txt);
std::vectorstd::string vec5(num);

for (auto&& elem : vec5)
	elem = "";
std::cout << "for문안에 T 정의 \t\t";
start();
for (int i = 0; i < num; ++i)
{
	std::string a = vec[i];
	vec5[i] = a;
}
end();

for (auto&& elem : vec5)
	elem = "";
std::cout << "for문 밖에서 T 정의 후 선언 \t";
start();
std::string c;
for (int i = 0; i < num; ++i)
{
	c = vec[i];
	vec5[i] = c;
}
end();

for (auto&& elem : vec5)
elem = “”;
std::cout << “for문안에 const T& 정의 \t”;
start();
for (int i = 0; i < num; ++i)
{
const std::string& b = vec[i];
vec5[i] = b;
}
end();
}

1 Like

(1)방식이 (2)방식보다 빠른 이유가 무엇일까요 ? (유의미한 수준의 차이가 항상 발생합니다.)

일단 엄밀하게 보자면 (1) 방식의 경우 복사 생성자를 호출하고 (2) 방식의 경우 operator= 를 호출한다는 차이가 있겠지요. 이 때문에 차이가 발생하지 않을까요? 사실 제 생각에 최적화 옵션 키고 컴파일 하게 된다면 둘 차이는 그닥 없을 것 같습니다.

(3)번 방식이 생성이 비싼 string 객체의 경우 압도적으로 빠른데, 그렇다면 for문 안에서 변수를 사용할 때 일반적으로 (3)번 방식으로 코드를 작성하는것이 바람직한 코딩습관일까요 ?
(여기서 for문 안에서 변수의 사용은 어떤 상수(변하지 않는) 값들을 받아와 사용하는 상황을 얘기하는 것 입니다.)

네 참조만 할 것이라면 굳이 객체를 생성할 필요가 없겠지요. 따라서 위 처럼 상수 레퍼런스로 참조하는 것이 옳다고 봅니다. 더욱이 위 처럼 원소 전체를 iterate 할 거라면

for (const auto& v : vec) {
}

과 같은 꼴로 쓰면 됩니다.

덧붙여서 위 코드에서도 계속 원소들을 초기화 할 때

for (auto&& elem : vec5)
  elem = "";

와 같이 쓰셨는데 range-for 문에서 forwarding reference 받는 것은 별로 바람직하지는 않네요. forwarding 레퍼런스를 쓰면 도대체 위 for 문에서 무슨 일을 하려는지 가독성이 매우 떨어집니다. 그냥 상황에 따라 const auto& elem 이나 auto& elem 둘 중 하나만 쓰세요 되도록이면. forwarding reference 가 꼭 필요한 상황은 제가 알기론 vector<bool> 밖에 없습니다 :slight_smile:

덧붙여 최적화에 관심이 많으시다고 하셨는데, 어셈블리 코드를 이해하는 것은 필수 입니다. 컴퓨터에서 돌아가는 코드는 컴파일러가 생성한 어셈블리이지 C++ 코드가 아니기 때문이지요.

1 Like

진부한 얘기이지만 제가 할 수 있는 말이 이것밖에 없네요.

항상 감사합니다 !

1 Like