자바 제네릭을 공부하다가 제네릭이 C++ 의 template 과 비슷하다는 생각이 들었고, 문득 C++ template 과 자바 generic 이 코드 생성에서는 어떤 차이점이 있을지 궁금증이 생겼다.
C++ template 에서는 템플릿을 사용한 함수나 클래스를 작성만 하고, main 함수에서 해당 함수를 호출하거나 객체를 생성하지 않으면 해당 함수나 클래스에 해당하는 어셈블리어 코드가 생성되지 않는다.
즉, C++ 에서 템플릿을 사용할 때는 컴파일 시점에서 해당 템블릿에 어떤 타입을 사용했는지 검사한 후 동적으로 코드를 생성하게 된다는 것이다.
일반 함수와 템플릿을 사용한 함수의 어셈블리 코드를 확인하여 실제 어떻게 코드가 생성되는지 알아보자
템플릿을 사용하지 않은 일반 함수를 선언만 하고 호출하지 않았을 때 어셈블리 코드가 생성되는지 확인해보자
//gcc test.cpp -o output -lstdc++
#include <iostream>
using namespace std;
int sum(int a, int b) {
return a + b;
}
int main() {
}
일반 함수 sum(int a, int b) 을 선언한 후 main 함수에서 호출을 하지 않고 컴파일 후 objdump 툴을 사용하여 어셈블리 코드를 확인해 보면 함수 이름은 조금 변경되었지만, sum 함수에 대한 코드가 있는 것을 확인할 수 있다.
$ ls -al
total 32
drwxr-xr-x 2 magan20 magan20 4096 2월 12 17:46 .
drwxr-xr-x 9 magan20 magan20 4096 2월 6 11:15 ..
-rwxrwxr-x 1 magan20 magan20 17064 2월 12 17:46 output
-rw-r--r-- 1 magan20 magan20 100 2월 12 17:46 test.cpp
$ objdump -d output | grep sum -A12
0000000000001169 <_Z3sumii>:
1169: f3 0f 1e fa endbr64
116d: 55 push %rbp
116e: 48 89 e5 mov %rsp,%rbp
1171: 89 7d fc mov %edi,-0x4(%rbp)
1174: 89 75 f8 mov %esi,-0x8(%rbp)
1177: 8b 55 fc mov -0x4(%rbp),%edx
117a: 8b 45 f8 mov -0x8(%rbp),%eax
117d: 01 d0 add %edx,%eax
117f: 5d pop %rbp
1180: c3 retq
이번에는 템플릿을 사용한 함수에 대한 어셈블리 코드를 확인해보자.
템플릿 함수를 선언만 하고 main 함수에서 호출하지 않았을 때 어셈블리 코드는 어떻게 생성될까?
#include <iostream>
using namespace std;
template <typename T>
T sum(T num1, T num2) {
return num1 + num2;
}
int main() {
}
템플릿 함수 sum을 선언한 후 main 함수에서 호출하지 않고 컴파일 했을 때 sum 함수에 대한 어셈블리 코드가 생성되지 않은 것을 확인할 수 있다.
또한, 위쪽에서 일반 함수를 선언한 코드의 실행 파일 output 의 크기와 아래의 output 파일의 크기를 비교했을 때도 아래쪽 파일의 크기가 더 작은 것을 확인할 수 있다.
$ ls -al
total 32
drwxr-xr-x 2 magan20 magan20 4096 2월 12 17:44 .
drwxr-xr-x 9 magan20 magan20 4096 2월 6 11:15 ..
-rwxrwxr-x 1 magan20 magan20 17032 2월 12 17:44 output
-rw-r--r-- 1 magan20 magan20 128 2월 12 17:44 test.cpp
$ objdump -d output | grep sum -A12
$
이번에는 템플릿 함수를 선언한 후, 타입 1개에 대해 호출해보자
#include <iostream>
using namespace std;
template <typename T>
T sum(T num1, T num2) {
return num1 + num2;
}
int main() {
sum(10, 10);
sum(10, 10);
sum(10, 10);
}
템플릿 함수를 선언하고 int 타입에 대한 sum 함수를 3번 호출했다.
이 경우 Z3sumIiET_S0_S0 라는 이름의 함수에 대한 어셈블리코드가 생성된 것을 확인할 수 있다.
$ ls -al
total 32
drwxr-xr-x 2 magan20 magan20 4096 2월 12 17:52 .
drwxr-xr-x 9 magan20 magan20 4096 2월 6 11:15 ..
-rwxrwxr-x 1 magan20 magan20 17072 2월 12 17:52 output
-rw-r--r-- 1 magan20 magan20 170 2월 12 17:52 test.cpp
$ objdump -d output | grep sum -A12
000000000000120b <_Z3sumIiET_S0_S0_>:
120b: f3 0f 1e fa endbr64
120f: 55 push %rbp
1210: 48 89 e5 mov %rsp,%rbp
1213: 89 7d fc mov %edi,-0x4(%rbp)
1216: 89 75 f8 mov %esi,-0x8(%rbp)
1219: 8b 55 fc mov -0x4(%rbp),%edx
121c: 8b 45 f8 mov -0x8(%rbp),%eax
121f: 01 d0 add %edx,%eax
1221: 5d pop %rbp
1222: c3 retq
1223: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
이제 템플릿 함수는 호출할 때 컴파일러에서 검사 후 어셈블리 코드를 생성한다는 것을 알게 되었다.
그럼 여러개의 타입에 대해 함수가 호출되면 어셈블리 코드는 어떻게 생성이 될까?
#include <iostream>
using namespace std;
template <typename T>
T sum(T num1, T num2) {
return num1 + num2;
}
int main() {
sum(10, 10);
sum(10, 10);
sum(10.0f, 10.0f);
}
2번 실험과 동일한 템플릿 함수를 사용한 후 main 함수에서 마지막 함수를 호출할 때 int 가 아닌 float 값을 사용해보았다.
이 경우 Z3sumIiET_S0_S0 함수 이름와 별개로 Z3sumIfET_S0_S0 라는 이름의 어셈블리 코드가 생성되었으며, 2번 실험과 코드 줄 수는 동일함에도 불구하고 output의 크기가 17072 바이트에서 17112 로 커진 것을 확인할 수 있다.
$ ls -al
total 32
drwxr-xr-x 2 magan20 magan20 4096 2월 12 17:59 .
drwxr-xr-x 9 magan20 magan20 4096 2월 6 11:15 ..
-rwxrwxr-x 1 magan20 magan20 17112 2월 12 17:59 output
-rw-r--r-- 1 magan20 magan20 176 2월 12 17:59 test.cpp
$ objdump -d output | grep sum -A12
0000000000001211 <_Z3sumIiET_S0_S0_>:
1211: f3 0f 1e fa endbr64
1215: 55 push %rbp
1216: 48 89 e5 mov %rsp,%rbp
1219: 89 7d fc mov %edi,-0x4(%rbp)
121c: 89 75 f8 mov %esi,-0x8(%rbp)
121f: 8b 55 fc mov -0x4(%rbp),%edx
1222: 8b 45 f8 mov -0x8(%rbp),%eax
1225: 01 d0 add %edx,%eax
1227: 5d pop %rbp
1228: c3 retq
0000000000001229 <_Z3sumIfET_S0_S0_>:
1229: f3 0f 1e fa endbr64
122d: 55 push %rbp
122e: 48 89 e5 mov %rsp,%rbp
1231: f3 0f 11 45 fc movss %xmm0,-0x4(%rbp)
1236: f3 0f 11 4d f8 movss %xmm1,-0x8(%rbp)
123b: f3 0f 10 45 fc movss -0x4(%rbp),%xmm0
1240: f3 0f 58 45 f8 addss -0x8(%rbp),%xmm0
1245: 5d pop %rbp
1246: c3 retq
1247: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
124e: 00 00
#include <iostream>
using namespace std;
template <typename T>
T sum(T num1, T num2) {
return num1 + num2;
}
int main() {
sum(10, 10);
sum(10L, 10L);
sum(10.0f, 10.0f);
}
long 타입에 대한 인자를 추가로 사용했을 때도 어셈블리 코드가 추가로 생성되고, 실행파일의 바이트 수도 늘어난 것을 확인할 수 있다.
$ ls -al
total 32
drwxr-xr-x 2 magan20 magan20 4096 2월 12 18:05 .
drwxr-xr-x 9 magan20 magan20 4096 2월 6 11:15 ..
-rwxrwxr-x 1 magan20 magan20 17152 2월 12 18:05 output
-rw-r--r-- 1 magan20 magan20 178 2월 12 18:05 test.cpp
$ objdump -d output | grep sum -A12
0000000000001211 <_Z3sumIiET_S0_S0_>:
1211: f3 0f 1e fa endbr64
1215: 55 push %rbp
1216: 48 89 e5 mov %rsp,%rbp
1219: 89 7d fc mov %edi,-0x4(%rbp)
121c: 89 75 f8 mov %esi,-0x8(%rbp)
121f: 8b 55 fc mov -0x4(%rbp),%edx
1222: 8b 45 f8 mov -0x8(%rbp),%eax
1225: 01 d0 add %edx,%eax
1227: 5d pop %rbp
1228: c3 retq
0000000000001229 <_Z3sumIlET_S0_S0_>:
1229: f3 0f 1e fa endbr64
122d: 55 push %rbp
122e: 48 89 e5 mov %rsp,%rbp
1231: 48 89 7d f8 mov %rdi,-0x8(%rbp)
1235: 48 89 75 f0 mov %rsi,-0x10(%rbp)
1239: 48 8b 55 f8 mov -0x8(%rbp),%rdx
123d: 48 8b 45 f0 mov -0x10(%rbp),%rax
1241: 48 01 d0 add %rdx,%rax
1244: 5d pop %rbp
1245: c3 retq
0000000000001246 <_Z3sumIfET_S0_S0_>:
1246: f3 0f 1e fa endbr64
124a: 55 push %rbp
124b: 48 89 e5 mov %rsp,%rbp
124e: f3 0f 11 45 fc movss %xmm0,-0x4(%rbp)
1253: f3 0f 11 4d f8 movss %xmm1,-0x8(%rbp)
1258: f3 0f 10 45 fc movss -0x4(%rbp),%xmm0
125d: f3 0f 58 45 f8 addss -0x8(%rbp),%xmm0
1262: 5d pop %rbp
1263: c3 retq
1264: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
126b: 00 00 00
126e: 66 90 xchg %ax,%ax
앞서 말한것과 같이 C++에서 템플릿을 사용할 경우에는 선언만 할 시, 어셈블리어는 생성되지 않고, 해당 템플릿 함수를 호출할 때 사용한 타입에 해당하는 어셈블리 코드가 각각 생성되는 것을 확인할 수 있었다.
C++은 템플릿을 적용한 함수를 사용할 때 컴파일러에서 동적으로 코드를 생성해준다. 그럼 과연 자바는 코드를 어떻게 생성할까?
만약 객체를 생성하지 않아도 코드가 생성된다면 여러 타입을 사용할 때는 과연 C++과 동일하게 각각의 타입에 대한 코드가 생성될까? 아니면 말 그대로 하나의 코드를 Generic 하게 사용을 하는 것일까?
class Member {
String member;
void setMember(String member) {
this.member = member;
}
String getMember() {
return this.member;
}
}
public class Test {
public static void main(String[] args) {
Member m = new Member();
}
}
사실 javac 명령어로 컴파일 하자마자 깨달은 건데 자바는 한개의 클래스를 한개의 .class 파일로 관리하기 때문에 제네릭을 사용하든 말든 한개의 .class 파일만 생성되는게 당연했다.
어셈블리 레벨의 코드 생성은 또 다를수도 있겠지만 기계어 생성은 JVM 에서 하기 때문에 확인이 어려울 듯 하다.
일단 생성된 .class 파일의 바이트 코드를 확인하면 다음과 같다.
$ javac Test.java
$ ls -al;
total 20
drwxrwxr-x 2 magan20 magan20 4096 2월 12 18:30 .
drwxrwxr-x 3 magan20 magan20 4096 2월 12 18:10 ..
-rw-rw-r-- 1 magan20 magan20 394 2월 12 18:30 Member.class
-rw-rw-r-- 1 magan20 magan20 282 2월 12 18:30 Test.class
-rw-rw-r-- 1 magan20 magan20 237 2월 12 18:30 Test.java
$ javap -v -p -s Test.class
Classfile /home/magan20/java/test/Test.class
Last modified Feb 12, 2023; size 282 bytes
MD5 checksum 1493c20476c1c8761f5ddc3f4e9f9342
Compiled from "Test.java"
public class Test
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #4 // Test
super_class: #5 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Class #15 // Member
#3 = Methodref #2.#14 // Member."<init>":()V
#4 = Class #16 // Test
#5 = Class #17 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 Test.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Utf8 Member
#16 = Utf8 Test
#17 = Utf8 java/lang/Object
{
public Test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 13: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Member
3: dup
4: invokespecial #3 // Method Member."<init>":()V
7: astore_1
8: return
LineNumberTable:
line 15: 0
line 16: 8
}
SourceFile: "Test.java"
$ javap -v -p -s Member.class
Classfile /home/magan20/java/test/Member.class
Last modified Feb 12, 2023; size 394 bytes
MD5 checksum 9a944371a9d03ec277a3ab2a48638581
Compiled from "Test.java"
class Member
minor version: 0
major version: 55
flags: (0x0020) ACC_SUPER
this_class: #3 // Member
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #4.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#18 // Member.member:Ljava/lang/String;
#3 = Class #19 // Member
#4 = Class #20 // java/lang/Object
#5 = Utf8 member
#6 = Utf8 Ljava/lang/String;
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 setMember
#12 = Utf8 (Ljava/lang/String;)V
#13 = Utf8 getMember
#14 = Utf8 ()Ljava/lang/String;
#15 = Utf8 SourceFile
#16 = Utf8 Test.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = NameAndType #5:#6 // member:Ljava/lang/String;
#19 = Utf8 Member
#20 = Utf8 java/lang/Object
{
java.lang.String member;
descriptor: Ljava/lang/String;
flags: (0x0000)
Member();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
void setMember(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: (0x0000)
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field member:Ljava/lang/String;
5: return
LineNumberTable:
line 5: 0
line 6: 5
java.lang.String getMember();
descriptor: ()Ljava/lang/String;
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field member:Ljava/lang/String;
4: areturn
LineNumberTable:
line 9: 0
}
SourceFile: "Test.java"
Member.class 의 바이트 코드 내용을 보면 Member 클래스의 멤버와 생성자, getter, setter 에 대한 내용이 있는 것을 확인할 수 있다.
setMember 메소드의 경우에는 java.lang.String 값을 인자로 받고 있고, putfield 부분을 보면 Ljava/lang/String 타입인 member 가 설정되어 있는것을 확인할 수 있다.
Ljava/lang/String
- Ljava/lang/String 의미는 원본 String Value 값의 참조형을 의미
getMember 메소드는 java.lang.String 값을 반환하는 걸로 표현이 되어 있다. getfield 부분은 Ljava/lang/String 타입인 member 로 설정되어 있는것을 확인할 수 있다.
class Member<T> {
T member;
void setMember(T member) {
this.member = member;
}
T getMember() {
return this.member;
}
}
public class Test {
public static void main(String[] args) {
Member<String> m = new Member<>();
}
}
$ javac Test.java
$ ls -al
total 20
drwxrwxr-x 2 magan20 magan20 4096 2월 16 16:14 .
drwxrwxr-x 3 magan20 magan20 4096 2월 12 18:10 ..
-rw-rw-r-- 1 magan20 magan20 504 2월 16 16:14 Member.class
-rw-rw-r-- 1 magan20 magan20 282 2월 16 16:14 Test.class
-rw-rw-r-- 1 magan20 magan20 235 2월 16 16:14 Test.java
$ javap -v -p -s Test.class
Classfile /home/magan20/java/test/Test.class
Last modified Feb 16, 2023; size 282 bytes
MD5 checksum 1493c20476c1c8761f5ddc3f4e9f9342
Compiled from "Test.java"
public class Test
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #4 // Test
super_class: #5 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Class #15 // Member
#3 = Methodref #2.#14 // Member."<init>":()V
#4 = Class #16 // Test
#5 = Class #17 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 Test.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Utf8 Member
#16 = Utf8 Test
#17 = Utf8 java/lang/Object
{
public Test();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 13: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Member
3: dup
4: invokespecial #3 // Method Member."<init>":()V
7: astore_1
8: return
LineNumberTable:
line 15: 0
line 16: 8
}
SourceFile: "Test.java"
$ javap -v -p -s Member.class
Classfile /home/magan20/java/test/Member.class
Last modified Feb 16, 2023; size 504 bytes
MD5 checksum d152f3cac1d3458683cc3f2d4ede86db
Compiled from "Test.java"
class Member<T extends java.lang.Object> extends java.lang.Object
minor version: 0
major version: 55
flags: (0x0020) ACC_SUPER
this_class: #3 // Member
super_class: #4 // java/lang/Object
interfaces: 0, fields: 1, methods: 3, attributes: 2
Constant pool:
#1 = Methodref #4.#22 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#23 // Member.member:Ljava/lang/Object;
#3 = Class #24 // Member
#4 = Class #25 // java/lang/Object
#5 = Utf8 member
#6 = Utf8 Ljava/lang/Object;
#7 = Utf8 Signature
#8 = Utf8 TT;
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 setMember
#14 = Utf8 (Ljava/lang/Object;)V
#15 = Utf8 (TT;)V
#16 = Utf8 getMember
#17 = Utf8 ()Ljava/lang/Object;
#18 = Utf8 ()TT;
#19 = Utf8 <T:Ljava/lang/Object;>Ljava/lang/Object;
#20 = Utf8 SourceFile
#21 = Utf8 Test.java
#22 = NameAndType #9:#10 // "<init>":()V
#23 = NameAndType #5:#6 // member:Ljava/lang/Object;
#24 = Utf8 Member
#25 = Utf8 java/lang/Object
{
T member;
descriptor: Ljava/lang/Object;
flags: (0x0000)
Signature: #8 // TT;
Member();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
void setMember(T);
descriptor: (Ljava/lang/Object;)V
flags: (0x0000)
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field member:Ljava/lang/Object;
5: return
LineNumberTable:
line 5: 0
line 6: 5
Signature: #15 // (TT;)V
T getMember();
descriptor: ()Ljava/lang/Object;
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field member:Ljava/lang/Object;
4: areturn
LineNumberTable:
line 9: 0
Signature: #18 // ()TT;
}
Signature: #19 // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "Test.java"
Member.class 의 바이트 코드 내용을 확인해보면 일반 Member 클래스와 동일하게 멤버와 생성자, getter, setter 에 대한 내용이 있는 것을 확인할 수 있다.
setMember 메소드를 확인해보면 Ljava/lang/Object 타입의 T 값을 받아서 putfield 를 Ljava/lang/Object 타입의 member로 설정하는 것을 볼 수 있다.
getMember 메소드는 T 값을 반환하는 걸로 표현이 되어 있고, getfield 부분은 Ljava/lang/Object 타입인 member 로 설정되어 있다.
자바는 C++과 달리 제네릭 생성시 java.lang.Object 로 타입을 설정한 후 일반적인(제네릭한) .class 코드 하나를 생성한다. Object 클래스는 모든 객체의 부모이므로 어떤 객체든 인자로 받을 수가 있다. 그러므로 제네릭 클래스를 사용해서 객체 생성 시 각 타입마다 여러 코드가 만들어지는 것이 아닌 Object 타입에 다른 타입(클래스)가 대입되는 것이라고 이해하면 편할 것 같다.
이 때문에 기본 타입인 int, float 등의 타입은 제네릭에서 사용할 수 없고, Integer, Float 등의 wrapper 클래스를 사용해야 한다.
늘 잘읽고 있읍니다...