컴파일러가 로컬 휘발성 변수를 최적화 할 수 있습니까?


79

컴파일러가이를 최적화 할 수 있습니까 (C ++ 17 표준에 따라) :

int fn() {
    volatile int x = 0;
    return x;
}

이에?

int fn() {
    return 0;
}

그렇다면 그 이유는 무엇입니까? 그렇지 않다면 왜 안됩니까?


이 주제에 대한 몇 가지 생각이 있습니다. 현재 컴파일러 fn()는 스택에있는 지역 변수로 컴파일 한 다음 반환합니다. 예를 들어 x86-64에서 gcc는 다음을 생성합니다.

mov    DWORD PTR [rsp-0x4],0x0 // this is x
mov    eax,DWORD PTR [rsp-0x4] // eax is the return register
ret    

이제 내가 아는 한 표준은 로컬 휘발성 변수가 스택에 있어야한다고 말하지 않습니다. 따라서이 버전도 똑같이 좋습니다.

mov    edx,0x0 // this is x
mov    eax,edx // eax is the return
ret    

여기 edx상점 x. 하지만 이제 왜 여기서 멈추나요? edxeax둘 다 0이므로 다음 과 같이 말할 수 있습니다.

xor    eax,eax // eax is the return, and x as well
ret    

그리고 fn()최적화 된 버전으로 전환 했습니다. 이 변환이 유효합니까? 그렇지 않은 경우 어떤 단계가 유효하지 않습니까?


1
의견은 확장 된 토론을위한 것이 아닙니다. 이 대화는 채팅 으로 이동 되었습니다 .


@philipxy : "무엇을 생산할 수 있는가"가 아닙니다. 변환이 허용되는지 여부입니다. 왜냐하면 허용 되지 않으면 변환 된 버전을 생성 하지 않아야 하기 때문입니다.
geza

이 표준은 프로그램에 대해 구현시 준수해야하는 휘발성 및 기타 관찰 가능 항목에 대한 일련의 액세스를 정의합니다. 그러나 휘발성 수단에 대한 액세스는 구현에 따라 정의됩니다. 따라서 구현이 무엇을 생성 할 수 있는지 묻는 것은 무의미합니다. 그것은 생성하도록 정의 된 것을 생성합니다. 구현 동작에 대한 설명이 주어지면 원하는 다른 것을 찾을 수 있습니다. 하지만 시작하려면 하나가 필요합니다. 코드 생성은 표준 및 구현의 규칙을 충족해야하는 것 외에는 관련이 없기 때문에 실제로 표준의 관찰 가능한 규칙에 관심이있을 수 있습니다.
philipxy

1
@philipxy : 표준에 관한 질문을 명확히하겠습니다. 일반적으로 이러한 종류의 질문에 의해 암시됩니다. 나는 표준이 말하는 것에 관심이 있습니다.
geza

답변:


63

아니요. volatile객체에 대한 액세스 는 로컬과 전역 사이의 특별한 구분없이 I / O와 똑같이 관찰 가능한 동작으로 간주됩니다.

준수 구현에 대한 최소 요구 사항은 다음과 같습니다.

  • volatile객체에 대한 액세스 는 추상 기계의 규칙에 따라 엄격하게 평가됩니다.

[...]

이를 총체적으로 프로그램의 관찰 가능한 동작이라고합니다.

N3690, [intro.execution], ¶8

이것이 얼마나 정확하게 관찰되는지는 표준의 범위를 벗어나며 I / O 및 전역 volatile개체에 대한 액세스와 마찬가지로 구현 별 영역에 바로 해당 합니다. volatile"당신은 여기에서 일어나는 모든 일을 알고 있다고 생각하지만, 그렇지 않습니다. 저를 믿고 너무 똑똑하지 말고이 일을하세요. 왜냐하면 저는 당신의 프로그램에서 당신의 바이트로 제 비밀 일을하고 있기 때문입니다." 이것은 실제로 [dcl.type.cv] ¶7에 설명되어 있습니다 :

[참고 : volatile객체의 값이 구현에서 감지 할 수없는 수단으로 변경 될 수 있으므로 객체와 관련된 적극적인 최적화를 피하기위한 구현에 대한 힌트입니다. 또한 일부 구현의 경우 volatile은 객체에 액세스하기 위해 특수 하드웨어 명령이 필요함을 나타낼 수 있습니다. 자세한 의미는 1.9를 참조하십시오. 일반적으로 volatile의 의미는 C에서와 동일하게 C ++에서 의도됩니다. — end note]


2
이것이 가장 많이 찬성 된 질문이고 질문이 편집으로 확장되었으므로이 답변을 편집하여 새로운 최적화 예제를 논의하는 것이 좋을 것입니다.
hyde

정답은 "예"입니다. 이 답변은 추상 기계 관찰 가능 항목과 생성 된 코드를 명확하게 구분하지 않습니다. 후자는 구현에 따라 정의됩니다. 예를 들어 주어진 디버거와 함께 사용하기 위해 휘발성 객체는 메모리 및 / 또는 레지스터에 보장됩니다. 예를 들어 일반적으로 관련 대상 아키텍처에서 pragma가 지정한 특수 메모리 위치에서 휘발성 객체에 대한 쓰기 및 / 또는 읽기가 보장됩니다. 구현은 액세스가 코드에 반영되는 방식을 정의합니다. 객체 (들)가 "구현에 의해 감지되지 않는 수단으로 변경 될 수있는"방법과시기를 결정합니다. (질문에 대한 나의 의견을 참조하십시오.)
philipxy

12

이 루프는 관찰 가능한 동작이 없기 때문에 as-if 규칙에 따라 최적화 할 수 있습니다.

for (unsigned i = 0; i < n; ++i) { bool looped = true; }

이것은 다음을 수행 할 수 없습니다.

for (unsigned i = 0; i < n; ++i) { volatile bool looped = true; }

두 번째 루프는 반복 할 때마다 어떤 작업을 수행하므로 루프가 O (n) 시간이 걸립니다. 상수가 무엇인지는 모르겠지만 측정 할 수 있고 (다소) 알려진 시간 동안 바쁘게 반복하는 방법이 있습니다.

표준이 휘발성 물질에 대한 액세스가 순서대로 이루어져야한다고 말하고 있기 때문에 그렇게 할 수 있습니다. 이 경우 표준이 적용되지 않는다고 컴파일러가 결정했다면 버그 보고서를 제출할 권리가 있다고 생각합니다.

컴파일러가 looped레지스터 에 넣기 로 선택하면 그것에 대해 좋은 주장이 없다고 생각합니다. 그러나 여전히 모든 루프 반복에 대해 해당 레지스터의 값을 1로 설정해야합니다.


그래서, 당신은 마지막 말 xor ax, ax( ax로 간주됩니다 volatile x질문에) 버전을 유효 또는 무효? IOW, 질문에 대한 답은 무엇입니까?
hyde

@hyde : 제가 읽은 질문은 "변수를 제거 할 수 있습니까"였고 제 대답은 "아니요"입니다. 휘발성을 레지스터에 넣을 수 있는지 여부에 대한 질문을 제기하는 특정 x86 구현의 경우 완전히 확실하지 않습니다. 비록 그것이으로 축소 xor ax, ax되더라도, 그 opcode는 쓸모 없어 보임에도 제거 될 수없고 병합 될 수도 없습니다. 내 루프 예제에서 컴파일 된 코드는 xor ax, ax관찰 가능한 동작 규칙을 충족하기 위해 n 번 실행해야합니다 . 편집이 귀하의 질문에 답하기를 바랍니다.
rici

그래 ..., 문제는 편집에 의해 상당히 확장되었다,하지만 당신은 편집 후 대답 때문에, 나는 새 부품을 포함해야한다이 대답을 생각
하이드

2
@hyde : 사실, 컴파일러가 루프를 최적화하는 것을 피하기 위해 벤치 마크에서 그런 식으로 휘발성을 사용합니다. ) =이 : 정말 이것에 대해 괜찮아 희망 그래서
RICI

표준은 volatile객체 에 대한 작업 이 그 자체로 일종의 부작용 이라고 말합니다 . 구현은 실제 CPU 명령을 생성 할 필요가없는 방식으로 의미를 정의 할 수 있지만 휘발성 한정 객체에 액세스하는 루프에는 부작용이 있으므로 제거 할 수 없습니다.
supercat

10

나는 volatile관찰 가능한 I / O 를 의미 하는 완전한 이해에도 불구하고 다수의 의견에 반대하기를 간청합니다 .

이 코드가있는 경우 :

{
    volatile int x;
    x = 0;
}

나는 컴파일러가 믿을 세 이하를 최적화 할 뿐만-경우 규칙 , 가정 한다 :

  1. volatile변수는 달리 외부에서 가시가되지를 통해 (그런 일이 주어진 범위에 존재하지 않기 때문에 여기에 문제가 분명하지 않습니다) 예를 들어, 포인터

  2. 컴파일러는 외부에서 액세스 할 수있는 메커니즘을 제공하지 않습니다. volatile

그 이유는 단순히 기준 # 2로 인해 차이를 관찰 할 수 없다는 것입니다.

그러나 컴파일러에서 기준 # 2가 충족되지 않을 수 있습니다 ! 컴파일러 volatile 스택 분석과 같이 "외부"에서 변수를 관찰하는 것에 대한 추가 보장을 제공하려고 할 수 있습니다 . 이러한 상황에서, 행동은 정말 이다 멀리 최적화되지 않을 수 있으므로, 관찰.

이제 질문은 다음 코드가 위와 다른 코드입니까?

{
    volatile int x = 0;
}

나는 최적화와 관련하여 Visual C ++에서 이것에 대해 다른 동작을 관찰했다고 생각하지만, 그 이유가 무엇인지 완전히 확신하지 못합니다. 초기화가 "액세스"로 간주되지 않을 수 있습니까? 잘 모르겠습니다. 관심이 있으시면 별도의 질문을 할 가치가 있지만 그렇지 않으면 위에서 설명한대로 대답이 있다고 생각합니다.


6

이론적으로 인터럽트 핸들러는

  • 반환 주소가 fn()함수 내에 있는지 확인하십시오 . 인스 트루먼 테이션 또는 첨부 된 디버그 정보를 통해 기호 테이블 또는 소스 행 번호에 액세스 할 수 있습니다.
  • 그런 다음 x스택 포인터에서 예측 가능한 오프셋에 저장되는 값을 변경합니다 .

… 따라서 fn()0이 아닌 값 을 반환합니다.


1
또는 .NET에서 중단 점을 설정하여 디버거를 사용하여이 작업을 더 쉽게 수행 할 수 있습니다 fn(). 를 사용 volatile하면 gcc -O0해당 변수 와 유사한 코드 생성이 생성 됩니다. 모든 C 문 사이에 유출 / 재로드가 있습니다. ( -O0아직 디버거의 일관성을 파괴하지 않고 하나 개의 문장 내에서 여러 접근을 결합 할 수 있지만, volatile그렇게 할 수 없습니다.)
피터 코르에게

또는 디버거를 사용하여 더 쉽게 :) 그러나 어떤 표준이 해당 변수를 관찰 할 수 있어야한다고 말합니까? 내 말은, 구현은 관찰 가능해야한다는 것을 선택할 수 있습니다. 또 다른 사람은 관찰 할 수 없다고 말할 수 있습니다. 후자는 표준을 위반합니까? 아마. 표준에 의해 지정되지 않았으며, 지역 휘발성 변수를 어떻게 관찰 할 수 있습니까?
geza

심지어 "관찰 가능"이란 무엇을 의미합니까? 스택에 배치해야합니까? 레지스터가 유지되면 어떻게 x됩니까? x86-64 xor rax, rax에서 0을 유지하면 (즉, 반환 값 레지스터가 유지됨 x), 물론 디버거에서 쉽게 관찰 / 수정할 수 있습니다 (즉, x에 저장된 디버그 기호 정보 유지 rax). 이것은 표준을 위반합니까?
geza

2
−1에 대한 모든 호출 fn()은 인라인 될 수 있습니다. MSVC 2017 및 기본 릴리스 모드에서는 그렇습니다. 그러면 " fn()기능 내부"가 없습니다 . 그럼에도 불구하고 변수는 자동 저장이므로 "예측 가능한 오프셋"이 없습니다.
건배와 hth. - 알프

1
0 @berendi : 네 말이 맞아요. 제가 틀렸어요. 죄송합니다, 그 점에서 나에게 나쁜 아침입니다 (두 번 잘못되었습니다). 그럼에도 불구하고 컴파일러가 다른 소프트웨어를 통한 액세스를 지원할 수있는 방법을 주장하는 것은 IMO가 쓸모 가 없습니다. volatile왜냐하면.와 상관없이 그렇게 할 수 있고 volatile그 지원을 제공하도록 강요하지 않기 때문 입니다. 그래서 저는 반대표를 제거합니다 (제가 틀 렸습니다). 그러나 저는 찬성하지 않습니다. 왜냐하면 저는이 추론이 명확하지 않다고 생각하기 때문입니다.
건배와 hth. - 알프

6

as-if 규칙과 volatile 키워드에 대한 자세한 참조를 추가 할 것 입니다. (이 페이지의 맨 아래에있는 "참조"및 "참조"를 따라 원래 사양을 추적하십시오.하지만 cppreference.com을 읽고 이해하기가 훨씬 더 쉽습니다.)

특히이 섹션을 읽어 주시기 바랍니다.

volatile 객체-유형이 volatile로 한정된 객체, 휘발성 객체의 하위 객체 또는 const-volatile 객체의 변경 가능한 하위 객체입니다. volatile로 한정된 유형의 glvalue 표현식을 통해 이루어진 모든 액세스 (읽기 또는 쓰기 작업, 멤버 함수 호출 등)는 최적화 목적 (즉, 단일 실행 스레드 내에서 휘발성)을 위해 가시적 인 부작용으로 처리됩니다. 액세스는 휘발성 액세스 이전 또는 이후에 순서가 지정된 다른 가시적 부작용으로 최적화되거나 재정렬 될 수 없습니다. 이는 휘발성 객체를 신호 처리기와의 통신에 적합하게 만들지 만 다른 실행 스레드와는 그렇지 않습니다. std :: memory_order를 참조하십시오. ). 비 휘발성 glvalue를 통해 (예 : 비 휘발성 유형에 대한 참조 또는 포인터를 통해) 휘발성 객체를 참조하려고하면 정의되지 않은 동작이 발생합니다.

따라서 volatile 키워드는 특히 glvalues 에서 컴파일러 최적화를 비활성화하는 입니다. 여기서 volatile 키워드가 영향을 미칠 수있는 유일한 것은 return x컴파일러가 나머지 함수로 원하는 모든 작업을 수행 할 수 있다는 것입니다.

컴파일러가 반환을 최적화 할 수있는 정도는이 경우 컴파일러가 x의 액세스를 최적화 할 수있는 정도에 따라 다릅니다 (아무것도 재정렬하지 않고 엄밀히 말하면 반환 표현식을 제거하지 않기 때문입니다. ,하지만 스택에 읽고 쓰는 것이므로 간소화 할 수 있어야합니다.) 그래서 제가 읽을 때 이것은 컴파일러가 최적화 할 수있는 정도의 회색 영역이며 두 가지 방법으로 쉽게 논쟁 할 수 있습니다.

참고 : 이러한 경우 항상 컴파일러가 원하는 / 필요한 것과 반대되는 작업을 수행한다고 가정합니다. 최적화를 비활성화하거나 (적어도이 모듈의 경우) 원하는 동작에 대해보다 정의 된 동작을 찾아야합니다. (이것이 유닛 테스트가 중요한 이유이기도합니다.) 결함이라고 생각되면 C ++ 개발자에게 문제를 제기해야합니다.


이 모든 것은 여전히 ​​읽기가 정말 어렵 기 때문에 내가 관련성이 있다고 생각하는 것을 포함하여 직접 읽을 수 있도록 노력하십시오.

glvalue glvalue 표현식은 lvalue 또는 xvalue입니다.

속성 :

glvalue는 lvalue에서 rvalue로, 배열에서 포인터로 또는 함수에서 포인터로 암시 적으로 변환하는 prvalue로 암시 적으로 변환 될 수 있습니다. glvalue는 다형성 일 수 있습니다. 식별하는 객체의 동적 유형이 반드시 표현식의 정적 유형은 아닙니다. glvalue는 표현식에서 허용하는 불완전한 유형을 가질 수 있습니다.


xvalue 다음 표현식은 xvalue 표현식입니다.

반환 유형이 객체에 대한 rvalue 참조 인 함수 호출 또는 오버로드 된 연산자 표현식 (예 : std :: move (x); a [n], 내장 첨자 표현식. 여기서 하나의 피연산자는 배열 rvalue입니다. am, 객체 표현식의 멤버. 여기서 a는 rvalue이고 m은 비 참조 유형의 비 정적 데이터 멤버입니다. a. * mp, 객체 표현식의 멤버에 대한 포인터. 여기서 a는 rvalue이고 mp는 데이터 멤버에 대한 포인터입니다. ㅏ ? b : c, 일부 b 및 c에 대한 삼항 조건식 (자세한 내용은 정의 참조); static_cast (x)와 같은 객체 유형에 대한 rvalue 참조에 대한 캐스트 표현식 임시 구체화 후 임시 객체를 지정하는 표현식입니다. (C ++ 17 이후) 속성 :

rvalue (아래)와 동일합니다. glvalue (아래)와 동일합니다. 특히, 모든 rvalue와 마찬가지로 xvalue는 rvalue 참조에 바인딩되며 모든 glvalue와 마찬가지로 xvalue는 다형성 일 수 있으며 비 클래스 xvalue는 cv-qualified 일 수 있습니다.


lvalue 다음 표현식은 lvalue 표현식입니다.

std :: cin 또는 std :: endl과 같이 유형에 관계없이 변수, 함수 또는 데이터 멤버의 이름. 변수의 유형이 rvalue 참조 인 경우에도 이름으로 구성된 표현식은 lvalue 표현식입니다. 반환 유형이 lvalue 참조 인 함수 호출 또는 오버로드 된 연산자 표현식 (예 : std :: getline (std :: cin, str), std :: cout << 1, str1 = str2 또는 ++ it); a = b, a + = b, a % = b 및 기타 모든 내장 할당 및 복합 할당 표현식; ++ a 및 --a, 내장 사전 증가 및 사전 감소 표현식; * p, 내장 간접 표현식; a [n] 및 p [n], 내장 첨자 표현식, 단 a가 배열 rvalue (C ++ 11부터) 인 경우 제외. am, 객체 표현식의 멤버 (m이 멤버 열거 자 또는 비 정적 멤버 함수 인 경우 제외) 또는 a는 rvalue이고 m은 비 참조 유형의 비 정적 데이터 멤버입니다. p-> m, 포인터 표현식의 내장 멤버 (m이 멤버 열거 자 또는 비 정적 멤버 함수 인 경우 제외). a. * mp, 객체 표현식의 멤버에 대한 포인터. 여기서 a는 lvalue이고 mp는 데이터 멤버에 대한 포인터입니다. p-> * mp, 포인터 표현식의 멤버에 대한 내장 포인터. 여기서 mp는 데이터 멤버에 대한 포인터입니다. a, b, 내장 쉼표 표현식, 여기서 b는 lvalue입니다. ㅏ ? b : c, 일부 b 및 c에 대한 삼항 조건식 (예 : 둘 다 동일한 유형의 l 값이지만 자세한 내용은 정의 참조) "Hello, world!"와 같은 문자열 리터럴; lvalue 참조 유형에 대한 캐스트 표현식 (예 : static_cast (x); 함수 호출 또는 오버로드 된 연산자 표현식, 반환 유형은 함수에 대한 rvalue 참조입니다. static_cast (x)와 같은 함수 유형에 대한 rvalue 참조에 대한 캐스트 표현식. (C ++ 11 이후) 속성 :

glvalue (아래)와 동일합니다. lvalue의 주소를 사용할 수 있습니다. & ++ i 1 및 & std :: endl은 유효한 표현식입니다. 수정 가능한 lvalue는 내장 할당 및 복합 할당 연산자의 왼쪽 피연산자로 사용될 수 있습니다. lvalue는 lvalue 참조를 초기화하는 데 사용할 수 있습니다. 이렇게하면 새 이름이 식으로 식별되는 개체와 연결됩니다.


as-if 규칙

C ++ 컴파일러는 다음 사항이 적용되는 한 프로그램에 대한 모든 변경을 수행 할 수 있습니다.

1) 모든 시퀀스 포인트에서 모든 휘발성 객체의 값이 안정적입니다 (이전 평가가 완료되고 새로운 평가가 시작되지 않음) (C ++ 11까지) 1) 휘발성 객체에 대한 액세스 (읽기 및 쓰기)는 의미 체계에 따라 엄격하게 발생합니다. 발생하는 표현의. 특히 동일한 스레드의 다른 휘발성 액세스와 관련하여 순서가 변경되지 않습니다. (C ++ 11부터) 2) 프로그램 종료시 파일에 기록 된 데이터는 프로그램이 기록 된대로 실행 된 것과 동일합니다. 3) 프로그램이 입력을 기다리기 전에 대화 형 장치로 전송되는 프롬프트 텍스트가 표시됩니다. 4) ISO C pragma #pragma STDC FENV_ACCESS가 지원되고 ON으로 설정된 경우,


사양을 읽으려면 읽어야 할 사양이라고 생각합니다.

참고 문헌

C11 표준 (ISO / IEC 9899 : 2011) : 6.7.3 유형 한정자 (p : 121-123)

C99 표준 (ISO / IEC 9899 : 1999) : 6.7.3 유형 한정자 (p : 108-110)

C89 / C90 표준 (ISO / IEC 9899 : 1990) : 3.5.3 유형 한정자


표준에 따르면 옳지 않을 수도 있지만 스택에 의존하여 실행 중에 다른 무언가를 만지는 사람은 코딩을 중지해야합니다. 나는 그것이 표준 결함이라고 주장합니다.
meneldal

1
@meneldal : 너무 광범위한 주장입니다. _AddressOfReturnAddress예를 들어 사용 에는 스택 분석이 포함됩니다. 사람들은 타당한 이유로 스택을 분석하며, 기능 자체가 정확성을 위해 스택에 의존하기 때문에 반드시 그런 것은 아닙니다.
user541686 jul.

1
glvalue은 여기에 있습니다 :return x;
게자

@geza 죄송합니다, 이것은 모두 읽기 어렵습니다. x가 변수이기 때문에 glvalue입니까? 또한 "최적화 할 수 없음"의 경우 컴파일러가 전혀 최적화 할 수 없거나 표현식을 변경하여 최적화 할 수 없다는 의미입니까? (컴파일러가 유지 관리 할 액세스 순서가 없기 때문에 여전히 여기에서 최적화 할 수있는 것처럼 읽습니다. 표현식은 여전히 ​​최적화 된 방식으로 해결되고 있습니다.) 명세서.
Tezra

여기에 :) 자신의 대답에서 인용의 "다음 식은 좌변 식이다 : 변수의 이름은 ..."
게자

-1

휘발성에 대한 포인터가 아닌 휘발성을 사용하는 지역 변수를 본 적이 없다고 생각합니다. 에서와 같이 :

int fn() {
    volatile int *x = (volatile int *)0xDEADBEEF;
    *x = 23;   // request data, 23 = temperature 
    return *x; // return temperature
}

내가 아는 휘발성의 유일한 다른 경우는 신호 처리기에 작성된 전역을 사용합니다. 거기에 관련된 포인터가 없습니다. 또는 하드웨어와 관련된 특정 주소에있는 링커 스크립트에 정의 된 기호에 액세스합니다.

최적화가 관찰 가능한 효과를 변경하는 이유를 추론하는 것이 훨씬 쉽습니다. 그러나 동일한 규칙이 로컬 휘발성 변수에 적용됩니다. 컴파일러는 x에 대한 액세스가 관찰 가능한 것처럼 동작해야하며이를 최적화 할 수 없습니다.


3
그러나 그것은 지역 휘발성 변수가 아니며 잘 알려진 주소의 휘발성 int에 대한 지역 비 휘발성 포인터입니다.
쓸모 없음

올바른 행동에 대해 추론하기가 더 쉽습니다. 언급했듯이 휘발성에 액세스하는 규칙은 지역 변수와 역 참조되는 휘발성 변수에 대한 포인터에 대해 동일합니다.
Goswin von Brederlow

x귀하의 코드에 "로컬 휘발성 변수"가 있음을 시사하는 것으로 보이는 답변의 첫 번째 문장을 다루고 있습니다. 그렇지 않습니다.
쓸모

int fn (const volatile int argument)이 컴파일되지 않았을 때 화가났습니다.
Joshua

4
편집은 귀하의 대답을 틀리지 않게 만들지 만 단순히 질문에 대한 대답이 아닙니다. 이것은에 대한 교과서 사용 사례이며 volatile로컬 인 것과는 아무 관련이 없습니다. 그것은 static volatile int *const x = ...글로벌 범위에있을 수 있고 당신이 말하는 모든 것은 여전히 ​​똑같을 것입니다. 이것은 질문을 이해하는 데 필요한 추가 배경 지식과 같습니다. 모든 사람이 가지고있는 것은 아니지만 실제 답은 아닙니다.
Peter Cordes jul.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.