volatile 을 흔히, 캐쉬를 사용하지 않고 메모리에 직접 접근하는 키워드라고 설명한다.
이는 코드의 해석을 쉽고 직관적이게 하기 위해서 훌륭한 설명이다.
하지만, 결과는 대부분 맞을지언정 틀린 설명이다.
캐쉬, 동기화 와 관련해서 하드웨어의 밑바닥까지 들여다보면 그 인생 고달파진다.
하지만 혹시나 그러고 싶은 사람들을 위해 준비했다.
[[[Volatile의 진짜 의미]]]
volatile에 대해 논하기 전에, cache coherence 라는것을 들어보았는가?
서로 다른 코어 간 캐쉬 불일치 문제는 하드웨어선에서 "언제나" 보장된다.
즉, volatile 키워드가 없어도 서로 다른 두개의 L1 캐쉬의 내용은 항상 동일하다.
같은 메모리에 A가 읽고, B가 읽고, A가 쓰고, B가 두번째 읽으면
A가 쓸때 B의 캐쉬가 인밸리데이트되며, B가 두번째 읽을때 메인메모리가 아닌 A의 캐쉬로 가서 읽는다.
즉, 언제나 B는 A가 쓴 최신의 값을 읽는다.
이게 하드웨어에 자동으로 구현되어있다.
자, 이제 volatile이 뭔가 이상하다는걸 알 수있다.
volatile은 실제로 유의미한 동기화작업을 한다.
그렇다면,
1. cache coherence에도 불구하고 발생하는 동기화 이슈는 무엇인가.
2. volatile이 어떻게 그것을 방지하는가?
두가지 측면으로 생각을 진행해보자.
가장 윗단의 캐쉬가 무엇이라고 생각하는가?
L1캐쉬?
데이터는 L1캐쉬로부터 가져오고
L1캐쉬보다 더 가까운 캐쉬는 없는가?
사실 모든 문제는 여기에서 출발한다. L1캐쉬가 가장 가까운 공간이 아니다.
캐쉬 코히어런스는 프라이빗캐쉬인 L1캐쉬들의 동기화를 진행하지만,
우리는 L1캐쉬에만 캐쉬를 의존하지 않는다.
우리는 메모리로부터 읽어온 값을, 레지스터에 저장하거나, 다른 공간(스택, 혹은 쓰레드, 프로세스 프라이빗메모리)에 저장한다.
이 또한 캐쉬다.
값이 언제 변할지 모르는 공유메모리에서, 값을 안전하고 논리적으로 더 가까운 공간으로 옮긴다는 말이다.
공유메모리의 L1캐쉬보다, 사적 메모리의 L1캐쉬가 더 가깝다.
공유메모리의 L1캐쉬보다, 레지스터가 더 가깝다.
C와 자바 둘을 예시로 들어보겠다.
C는 포인터로부터 읽은 값을 레지스터에 옮겨담고, 레지스터에서 연산을 진행하곤 한다.
캐쉬코히어런스는 레지스터까지 동기화하지 않는다.
읽은 값을 레지스터에 옮겨담은 시점에서, 레지스터의 값을 변경하는것은 다른 쓰레드에 반영되지 않는다.
우린 여전히 포인터의 주소에 값을 쓰지 않았다.
복사해온 레지스터만 변경했을 뿐이다.
아직, 공유데이터는 변하지 않았다.
하지만 논리적으로, 레지스터 값의 변경은 공유데이터의 값의 변경과 동일하게 처리되어야한다.
이에 대한 해결책은,
C가 연산할동안, 해당 포인터로부터 읽은 값을 복사해서 다른 공간에 저장하지 않고,
매번 포인터로부터 값을 받아와서 연산하는 것이다.
매번 포인터로부터 값을 쓰는것이다.
즉, volatile 은 메인메모리 상시접근이 아니다.
"L1캐쉬 상시 접근"이다.
그리고 매번 포인터로부터 값을 읽고 써서 L1캐쉬에 접근한다면,
앞서 이야기한 캐쉬 코히어런스에 의하여 동기화가 완벽하게 진행된다.
자바의 경우는 어떤가요?
자바는 레지스터에다가 변수를 저장하지 않는다.
그럼에도 자바도 문제가 터지는 연산을 한다.
자바의 메소드가 클래스의 필드를 참조한다고 해보자.
필드는 메모리위의 공간이 되며, 이 필드를 여러 쓰레드가 공유하고,
따라서 여러개의 L1캐쉬에 캐쉬되어있으며, 그 L1캐쉬들은 cache coherence에 의해 동기화가 진행된다.
JVM은 내부적으로 JVM스택이라는 공간을 쓴다.
메소드가 필드로부터 값을 읽거나 쓸때,
jvm스택 맨 위에 읽을 오브젝트를 갖다놓고
getfield {fieldname} {type} 인스트럭선을 진행해서 jvm스택 맨 위에다가 읽은 값을 저장한다.
이렇게 한번 값을 읽고나면,
이 값을 스택에서 이동해대고 지역변수슬롯에 저장해가면서 연산을 진행한다.
마찬가지로 값의 복사가 일어났다.
하드웨어는 복사된 값이 jvm 스택과 슬롯을 이동해댈동안, 그 값의 변동을 추적하지 않는다.
즉, jvm이 값을 복사해놓고 연산할 동안에는 데이터불일치가 발생한다.
마찬가지로, volatile을 써서
매번 getfield
putfield를 진행하도록 만들면 된다.
마찬가지로 매번 main memory 접근,
이 아닌
L1캐쉬 접근이다.
물론 소프트웨어적으로 main memory접근하는게
하드웨어적으로 L1캐쉬접근이므로 메인메모리접근도 틀린 소리는 아니다.
"다만, L1캐쉬를 안거치고 메인메모리 직접접근이 아니란거다"
이는 코드의 해석을 쉽고 직관적이게 하기 위해서 훌륭한 설명이다.
하지만, 결과는 대부분 맞을지언정 틀린 설명이다.
캐쉬, 동기화 와 관련해서 하드웨어의 밑바닥까지 들여다보면 그 인생 고달파진다.
하지만 혹시나 그러고 싶은 사람들을 위해 준비했다.
[[[Volatile의 진짜 의미]]]
volatile에 대해 논하기 전에, cache coherence 라는것을 들어보았는가?
서로 다른 코어 간 캐쉬 불일치 문제는 하드웨어선에서 "언제나" 보장된다.
즉, volatile 키워드가 없어도 서로 다른 두개의 L1 캐쉬의 내용은 항상 동일하다.
같은 메모리에 A가 읽고, B가 읽고, A가 쓰고, B가 두번째 읽으면
A가 쓸때 B의 캐쉬가 인밸리데이트되며, B가 두번째 읽을때 메인메모리가 아닌 A의 캐쉬로 가서 읽는다.
즉, 언제나 B는 A가 쓴 최신의 값을 읽는다.
이게 하드웨어에 자동으로 구현되어있다.
자, 이제 volatile이 뭔가 이상하다는걸 알 수있다.
volatile은 실제로 유의미한 동기화작업을 한다.
그렇다면,
1. cache coherence에도 불구하고 발생하는 동기화 이슈는 무엇인가.
2. volatile이 어떻게 그것을 방지하는가?
두가지 측면으로 생각을 진행해보자.
가장 윗단의 캐쉬가 무엇이라고 생각하는가?
L1캐쉬?
데이터는 L1캐쉬로부터 가져오고
L1캐쉬보다 더 가까운 캐쉬는 없는가?
사실 모든 문제는 여기에서 출발한다. L1캐쉬가 가장 가까운 공간이 아니다.
캐쉬 코히어런스는 프라이빗캐쉬인 L1캐쉬들의 동기화를 진행하지만,
우리는 L1캐쉬에만 캐쉬를 의존하지 않는다.
우리는 메모리로부터 읽어온 값을, 레지스터에 저장하거나, 다른 공간(스택, 혹은 쓰레드, 프로세스 프라이빗메모리)에 저장한다.
이 또한 캐쉬다.
값이 언제 변할지 모르는 공유메모리에서, 값을 안전하고 논리적으로 더 가까운 공간으로 옮긴다는 말이다.
공유메모리의 L1캐쉬보다, 사적 메모리의 L1캐쉬가 더 가깝다.
공유메모리의 L1캐쉬보다, 레지스터가 더 가깝다.
C와 자바 둘을 예시로 들어보겠다.
C는 포인터로부터 읽은 값을 레지스터에 옮겨담고, 레지스터에서 연산을 진행하곤 한다.
캐쉬코히어런스는 레지스터까지 동기화하지 않는다.
읽은 값을 레지스터에 옮겨담은 시점에서, 레지스터의 값을 변경하는것은 다른 쓰레드에 반영되지 않는다.
우린 여전히 포인터의 주소에 값을 쓰지 않았다.
복사해온 레지스터만 변경했을 뿐이다.
아직, 공유데이터는 변하지 않았다.
하지만 논리적으로, 레지스터 값의 변경은 공유데이터의 값의 변경과 동일하게 처리되어야한다.
이에 대한 해결책은,
C가 연산할동안, 해당 포인터로부터 읽은 값을 복사해서 다른 공간에 저장하지 않고,
매번 포인터로부터 값을 받아와서 연산하는 것이다.
매번 포인터로부터 값을 쓰는것이다.
즉, volatile 은 메인메모리 상시접근이 아니다.
"L1캐쉬 상시 접근"이다.
그리고 매번 포인터로부터 값을 읽고 써서 L1캐쉬에 접근한다면,
앞서 이야기한 캐쉬 코히어런스에 의하여 동기화가 완벽하게 진행된다.
자바의 경우는 어떤가요?
자바는 레지스터에다가 변수를 저장하지 않는다.
그럼에도 자바도 문제가 터지는 연산을 한다.
자바의 메소드가 클래스의 필드를 참조한다고 해보자.
필드는 메모리위의 공간이 되며, 이 필드를 여러 쓰레드가 공유하고,
따라서 여러개의 L1캐쉬에 캐쉬되어있으며, 그 L1캐쉬들은 cache coherence에 의해 동기화가 진행된다.
JVM은 내부적으로 JVM스택이라는 공간을 쓴다.
메소드가 필드로부터 값을 읽거나 쓸때,
jvm스택 맨 위에 읽을 오브젝트를 갖다놓고
getfield {fieldname} {type} 인스트럭선을 진행해서 jvm스택 맨 위에다가 읽은 값을 저장한다.
이렇게 한번 값을 읽고나면,
이 값을 스택에서 이동해대고 지역변수슬롯에 저장해가면서 연산을 진행한다.
마찬가지로 값의 복사가 일어났다.
하드웨어는 복사된 값이 jvm 스택과 슬롯을 이동해댈동안, 그 값의 변동을 추적하지 않는다.
즉, jvm이 값을 복사해놓고 연산할 동안에는 데이터불일치가 발생한다.
마찬가지로, volatile을 써서
매번 getfield
putfield를 진행하도록 만들면 된다.
마찬가지로 매번 main memory 접근,
이 아닌
L1캐쉬 접근이다.
물론 소프트웨어적으로 main memory접근하는게
하드웨어적으로 L1캐쉬접근이므로 메인메모리접근도 틀린 소리는 아니다.
"다만, L1캐쉬를 안거치고 메인메모리 직접접근이 아니란거다"
말이 길어졌는데, 결국 volatile이 하는 일은 복사금지임. 해당 메모리의 값을 프라이빗하게 복사해가지 말란소리.
이 글은 하드웨어 멀티코어에 대한 이해, C컴파일러가 코드를 어떻게 어셈블리로 변환하는지에 대한 이해, JVM 바이트코드에 대한 이해, vollatile 키워드의 작용에 대한 이해 가 있어야 쓸 수 있고, 구글링해도 제대로된 답변은 없음. 부분적으로만 맞는소리들을 하는데, 전체적인 통합적인 이해와 따라서 완벽히 본질을 꿰뚫는 통찰이 없음. 왜 없는지 모르겠
하드웨어 수준까지 내려가서 인스트럭션을 다루어야하는 예시가 바로 이 volatile에 대한 이해임. volatile이 정확히 어떻게 작용하지? 이러면 안전한가? 하는걸 정확히 분석하려면 결국 하드웨어까지 내려가야함. 하드웨어얘기를 안하고 넘기니까 캐쉬사용안하고 직접접근 이라는 오정보가 생기는것
얘는 보면 똑똑한 것 같다가도 이상하고 그래
이거할줄알면 어디다니고 얼마받아 ?
volatile 이용하면 매번 연산할때마다 load store을 한다는거 맞지? 이거때문에 동기화 문제 생기는거였구나 - dc App
쿠다 옛날 버전은 volatile 안써도 warp내에서 동기화 되던데 그러면 옛날엔 같은 메모리에 접근해도 load store 반복하도록 컴파일 됐던건가 - dc App
쿠다는 나도 이제 막 입문하려는 (그리고 대충 뭐하는지만 보고 진짜 입문은 뒤로 미루려는) 입장이라 이야기해줄수가 없고.volatile 쓰면, 매번 연산할떄마다 해당 메모리로부터 load store를 진행한다는거 맞음.단, 캐쉬를 안거치는게 아니고, 일반적인 메모리 접근처럼 캐쉬를 씀.공유 메모리에 항상 접근하는거임. 공유메모리에 있는 데이터를 복사해서 private 한 영역으로 옮겨담지 않는단 뜻임.a 에 접근해서 데이터를 가져오세요. 그리고 a 를 연산하세요. 라는 대목이 있을때컴파일러가 a에서 데이터를 가져오고, 그거를 레지스터에 저장한 다음, a를 연산하는 대목은 레지스터연산으로 진행할 수가 있음.a에 값을 기록하는거는, 나중에 연산이 다 끝나고 레지스터에서 a
로 다시 값을 옮겨담고.
이때 a 값을 공유되는 a에 직접 접근하는게 아니라, 레지스터에 가져다 놓고 프라이빗하게 쓰니까, 그 동안에는 다른 쓰레드한테 변경사항이 안보임. 레지스터가 변경되는거임. (나중에는 a에 다시 옮겨담겠지만, 그 시간 전까지는 다른 쓰레드한테 값의 변경이 안보임) 이렇게 a 값이랑 레지스터 값이랑 공간적으로 분리시키면 안되니까 a를 연산할때, 레지스터를 쓰지 말고 매번 a에 접근하라고 명시시키는거임. 그니까 결국 39.7 님 이해한 바가 맞음
cache를 flush하면 메인메모리 접근 가능